<!-- User Management Tab -->
<div id="tabAdminUsers" class="tab-content">
    <div class="endpoint-section" id="adminUsersList">
        <h2>
            <span class="endpoint-method get">GET</span>
            <span class="endpoint-path">/api/v1/admin/users - List Users</span>
        </h2>
        <p>List all users with pagination and filtering.</p>

        <div class="columns">
            <div class="column">
                <div class="form-group">
                    <label for="adminUsersList_limit">Limit:</label>
                    <input type="number" id="adminUsersList_limit" value="20" min="1" max="100">
                </div>

                <div class="form-group">
                    <label for="adminUsersList_offset">Offset:</label>
                    <input type="number" id="adminUsersList_offset" value="0" min="0">
                </div>
            </div>

            <div class="column">
                <div class="form-group">
                    <label for="adminUsersList_role">Filter by Role:</label>
                    <select id="adminUsersList_role">
                        <option value="">All Roles</option>
                        <option value="admin">Admin</option>
                        <option value="user">User</option>
                        <option value="moderator">Moderator</option>
                    </select>
                </div>

                <div class="form-group">
                    <label for="adminUsersList_status">Filter by Status:</label>
                    <select id="adminUsersList_status">
                        <option value="">All</option>
                        <option value="active">Active</option>
                        <option value="inactive">Inactive</option>
                        <option value="suspended">Suspended</option>
                    </select>
                </div>
            </div>
        </div>

        <button class="api-button" id="btnAdminUsersList">List Users</button>

        <h3>cURL Command:</h3>
        <pre id="adminUsersList_curl">---</pre>

        <h3>Response:</h3>
        <pre id="adminUsersList_response">---</pre>
    </div>

    <div class="endpoint-section" id="adminUserRegister">
        <h2>
            <span class="endpoint-method post">POST</span>
            <span class="endpoint-path">/api/v1/auth/register - Create User</span>
        </h2>
        <p>Create a new user account. In SQLite setups, an API key is generated and returned once.</p>

        <div class="columns">
            <div class="column">
                <div class="form-group">
                    <label for="adminReg_username">Username <span class="required">*</span>:</label>
                    <input type="text" id="adminReg_username" placeholder="newuser">
                </div>
                <div class="form-group">
                    <label for="adminReg_email">Email <span class="required">*</span>:</label>
                    <input type="email" id="adminReg_email" placeholder="user@example.com">
                </div>
            </div>
            <div class="column">
                <div class="form-group">
                    <label for="adminReg_password">Password <span class="required">*</span>:</label>
                    <input type="password" id="adminReg_password" placeholder="********">
                </div>
                <div class="form-group">
                    <label for="adminReg_code">Registration Code (optional):</label>
                    <input type="text" id="adminReg_code" placeholder="if required">
                </div>
            </div>
        </div>

        <button class="api-button" id="btnAdminCreateUser">Create User</button>

        <h3>Response:</h3>
        <pre id="adminUserRegister_response">---</pre>
    </div>



    <div class="endpoint-section" id="adminRegistrationCodes">
        <h2>
            <span class="endpoint-method get">GET/POST/DELETE</span>
            <span class="endpoint-path">/api/v1/admin/registration-codes - Manage Registration Codes</span>
        </h2>
        <p>Create, list, and revoke registration codes for controlled signups.</p>

        <div class="columns">
            <div class="column">
                <div class="form-group">
                    <label for="regCode_maxUses">Max Uses:</label>
                    <input type="number" id="regCode_maxUses" value="1" min="1" />
                </div>
                <div class="form-group">
                    <label for="regCode_expires">Expires in (days):</label>
                    <input type="number" id="regCode_expires" value="30" min="1" />
                </div>
            </div>
            <div class="column">
                <div class="form-group">
                    <label for="regCode_role">Role:</label>
                    <select id="regCode_role">
                        <option value="user" selected>user</option>
                        <option value="moderator">moderator</option>
                        <option value="admin">admin</option>
                    </select>
                </div>
                <div class="form-group">
                    <label for="regCode_desc">Description:</label>
                    <input type="text" id="regCode_desc" placeholder="Optional note" />
                </div>
            </div>
        </div>

        <div class="btn-group">
            <button class="api-button" id="btnRegCodeCreate">Create Code</button>
            <button class="api-button" id="btnRegCodeList">List Codes</button>
        </div>

        <h3>Latest Result:</h3>
        <pre id="adminRegCodes_result">---</pre>

        <h3>Codes:</h3>
        <div id="adminRegCodes_list"></div>
    </div>

    <!-- LLM Usage Reporting -->
    <div class="endpoint-section" id="adminLLMUsageList">
        <h2>
            <span class="endpoint-method get">GET</span>
            <span class="endpoint-path">/api/v1/admin/llm-usage - LLM Usage Log</span>
        </h2>
        <p>Query per-request LLM usage by user/provider/model/operation and time range.</p>

        <div class="columns">
            <div class="column">
                <div class="form-group">
                    <label for="adminLLM_user_id">User ID:</label>
                    <input type="number" id="adminLLM_user_id" placeholder="e.g., 1">
                </div>
                <div class="form-group">
                    <label for="adminLLM_provider">Provider:</label>
                    <input type="text" id="adminLLM_provider" placeholder="openai">
                </div>
                <div class="form-group">
                    <label for="adminLLM_model">Model:</label>
                    <input type="text" id="adminLLM_model" placeholder="gpt-4o">
                </div>
            </div>
            <div class="column">
                <div class="form-group">
                    <label for="adminLLM_operation">Operation:</label>
                    <select id="adminLLM_operation">
                        <option value="">All</option>
                        <option value="chat">chat</option>
                        <option value="embeddings">embeddings</option>
                        <option value="tts">tts</option>
                        <option value="stt">stt</option>
                    </select>
                </div>
                <div class="form-group">
                    <label for="adminLLM_status">HTTP Status:</label>
                    <input type="number" id="adminLLM_status" placeholder="e.g., 200">
                </div>
                <div class="form-group">
                    <label for="adminLLM_limit">Limit:</label>
                    <input type="number" id="adminLLM_limit" value="50" min="1" max="5000">
                </div>
            </div>
        </div>

        <div class="columns">
            <div class="column">
                <div class="form-group">
                    <label for="adminLLM_start">Start (date):</label>
                    <input type="date" id="adminLLM_start" placeholder="YYYY-MM-DD">
                </div>
            </div>
            <div class="column">
                <div class="form-group">
                    <label for="adminLLM_end">End (date):</label>
                    <input type="date" id="adminLLM_end" placeholder="YYYY-MM-DD">
                </div>
            </div>
        </div>

        <div class="btn-group">
            <button class="api-button" id="btnAdminLLMUsageQuery">Query</button>
            <button class="api-button" id="btnAdminLLMUsageCSV">Download CSV</button>
        </div>

        <h3>Latest Result:</h3>
        <pre id="adminLLMUsage_result">---</pre>
    </div>

    <!-- Inline script moved to js/admin-advanced.js -->

    <!-- Audit Export -->
    <div class="endpoint-section" id="adminAuditExport">
        <h2>
            <span class="endpoint-method get">GET</span>
            <span class="endpoint-path">/api/v1/audit/export - Audit Logs Export</span>
        </h2>
        <p>Export audit logs (JSON or CSV). Admin-only. Use presets or custom filters.</p>

        <div class="columns">
            <div class="column">
                <div class="form-group">
                    <label for="audit_format">Format:</label>
                    <select id="audit_format">
                        <option value="json">json</option>
                        <option value="csv">csv</option>
                    </select>
                </div>
                <div class="form-group">
                    <label for="audit_min_risk">Min Risk Score:</label>
                    <input type="number" id="audit_min_risk" placeholder="e.g., 70" min="0" max="100" />
                </div>
                <div class="form-group">
                    <label for="audit_user_id">User ID:</label>
                    <input type="text" id="audit_user_id" placeholder="optional" />
                </div>
            </div>
            <div class="column">
                <div class="form-group">
                    <label for="audit_event_type">Event Type(s):</label>
                    <input type="text" id="audit_event_type" placeholder="e.g., API_REQUEST,DATA_READ" />
                </div>
                <div class="form-group">
                    <label for="audit_category">Category(ies):</label>
                    <input type="text" id="audit_category" placeholder="e.g., API_CALL,SECURITY" />
                </div>
                <div class="form-group">
                    <label for="audit_filename">Filename:</label>
                    <input type="text" id="audit_filename" placeholder="audit_export.csv or .json" />
                </div>
            </div>
            <div class="column">
                <div class="form-group">
                    <label for="audit_start">Start Time (ISO):</label>
                    <input type="text" id="audit_start" placeholder="YYYY-MM-DDTHH:MM:SS+00:00" />
                </div>
                <div class="form-group">
                    <label for="audit_end">End Time (ISO):</label>
                    <input type="text" id="audit_end" placeholder="YYYY-MM-DDTHH:MM:SS+00:00" />
                </div>
            </div>
        </div>

        <div class="btn-group">
            <button class="api-button" id="btnAdminAuditDownload">Download</button>
            <button class="api-button" id="btnAdminAuditLast24h">Last 24h High-Risk (CSV)</button>
            <button class="api-button" id="btnAdminAuditApiEvents">API Events (CSV)</button>
            <button class="api-button" id="btnAdminAuditPreview">Preview JSON</button>
        </div>

        <h3>Preview:</h3>
        <pre id="adminAuditPreview">---</pre>

        <!-- Inline script moved to js/admin-advanced.js -->
    </div>

    <div id="tabMonitoring" class="tab-content">
        <div class="btn-group" style="margin-bottom: 12px;">
            <button class="api-button subtle danger" id="btnMonResetUI" title="Clear watchlist filters and notification drafts">Reset All UI</button>
        </div>
        <!-- Monitoring: Watchlists -->
        <div class="endpoint-section" id="monitoringWatchlists">
            <h2>
                <span class="endpoint-method get">GET/POST/DELETE</span>
                <span class="endpoint-path">/api/v1/monitoring/watchlists - Manage Watchlists</span>
            </h2>
            <p>Create and manage topic watchlists. Global watchlists apply to all users; per-user watchlists apply to the specified user ID.</p>

            <div class="btn-group">
                <button class="api-button" id="btnMonWlList">List</button>
                <button class="api-button" id="btnMonWlReload">Reload from file</button>
            </div>

            <h3>Current Watchlists</h3>
            <details>
                <summary>Filters</summary>
                <div class="columns">
                    <div class="column">
                        <div class="form-group">
                            <label for="monWl_filter_scope_type">Scope:</label>
                            <select id="monWl_filter_scope_type">
                                <option value="">All</option>
                                <option value="global">global</option>
                                <option value="user">user</option>
                                <option value="team">team</option>
                                <option value="org">org</option>
                            </select>
                        </div>
                    </div>
                    <div class="column">
                        <div class="form-group">
                            <label for="monWl_filter_scope_id">Scope ID:</label>
                            <input id="monWl_filter_scope_id" type="text" placeholder="id or *" />
                        </div>
                    </div>
                    <div class="column">
                        <div class="form-group">
                            <label>Columns:</label>
                            <label><input type="checkbox" id="monWl_col_id" checked /> ID</label>
                            <label><input type="checkbox" id="monWl_col_scope" checked /> Scope</label>
                            <label><input type="checkbox" id="monWl_col_rules" checked /> Rules</label>
                        </div>
                    </div>
                </div>
                <div class="column">
                    <div class="form-group">
                        <label>&nbsp;</label>
                        <button class="btn" id="monWl_filters_reset" type="button">Reset Filters</button>
                    </div>
                </div>
            </details>
            <div id="monitoringWatchlists_list"></div>

            <h3>Create/Update Watchlist</h3>
            <div class="columns">
                <div class="column">
                    <div class="form-group">
                        <label for="monWl_id">ID (leave blank to create new):</label>
                        <input id="monWl_id" type="text" placeholder="auto-generated if blank" />
                    </div>
                    <div class="form-group">
                        <label for="monWl_name">Name:</label>
                        <input id="monWl_name" type="text" placeholder="Kid-Safe Defaults - Custom" />
                    </div>
                    <div class="form-group">
                        <label for="monWl_desc">Description:</label>
                        <input id="monWl_desc" type="text" placeholder="Description" />
                    </div>
                </div>
                <div class="column">
                    <div class="form-group">
                        <label for="monWl_enabled">Enabled:</label>
                        <select id="monWl_enabled">
                            <option value="true">true</option>
                            <option value="false">false</option>
                        </select>
                    </div>
                    <div class="form-group">
                        <label for="monWl_scope_type">Scope Type:</label>
                        <select id="monWl_scope_type">
                            <option value="global" selected>global</option>
                            <option value="user">user</option>
                            <option value="team">team</option>
                            <option value="org">org</option>
                        </select>
                    </div>
                    <div class="form-group">
                        <label for="monWl_scope_id">Scope ID:</label>
                        <input id="monWl_scope_id" type="text" placeholder="* or user/team/org id" />
                    </div>
                </div>
            </div>

            <div class="form-group">
                <label for="monWl_rules">Rules (JSON array of {pattern,category,severity,note}):</label>
                <textarea id="monWl_rules" class="code-input" rows="8" placeholder='[{"pattern":"/\\bnsfw\\b/i","category":"adult","severity":"warning"}]'></textarea>
            </div>
            <div class="btn-group">
                <button class="api-button" id="btnMonWlSave">Save Watchlist</button>
                <button class="api-button danger" id="btnMonWlDelete">Delete Watchlist</button>
            </div>
            <pre id="monitoringWatchlists_result">---</pre>

            <h3>Quick Apply Kid-Safe Defaults to Team/Org</h3>
            <div class="columns">
                <div class="column">
                    <div class="form-group">
                        <label for="monQuick_team">Team ID:</label>
                        <input id="monQuick_team" type="text" placeholder="team id" />
                    </div>
                    <button class="api-button" id="btnMonQuickTeam">Apply to Team</button>
                </div>
                <div class="column">
                    <div class="form-group">
                        <label for="monQuick_org">Org ID:</label>
                        <input id="monQuick_org" type="text" placeholder="org id" />
                    </div>
                    <button class="api-button" id="btnMonQuickOrg">Apply to Org</button>
                </div>
            </div>

            <h3>Bulk Apply Kid-Safe Defaults</h3>
            <div class="columns">
                <div class="column">
                    <div class="form-group">
                        <label for="monBulk_scope">Scope Type:</label>
                        <select id="monBulk_scope"><option value="team">team</option><option value="org">org</option></select>
                    </div>
                </div>
                <div class="column">
                    <div class="form-group">
                        <label for="monBulk_ids">IDs (one per line or comma-separated):</label>
                        <textarea id="monBulk_ids" class="code-input" rows="4" placeholder="team123\nteam456 or org123,org456"></textarea>
                    </div>
                </div>
                <div class="column">
                    <div class="btn-group" style="margin-top:24px;">
                        <button class="api-button" id="btnMonBulkApply">Apply Defaults to IDs</button>
                    </div>
                </div>
            </div>
        </div>

        <!-- Monitoring: Alerts -->
        <div class="endpoint-section" id="monitoringAlerts">
            <h2>
                <span class="endpoint-method get">GET/POST</span>
                <span class="endpoint-path">/api/v1/monitoring/alerts - View and mark alerts read</span>
            </h2>
            <div class="columns">
                <div class="column">
                    <div class="form-group">
                        <label for="monAlerts_user">Filter user_id:</label>
                        <input id="monAlerts_user" type="text" placeholder="optional" />
                    </div>
                    <div class="form-group">
                        <label for="monAlerts_since">Since (ISO 8601):</label>
                        <input id="monAlerts_since" type="text" placeholder="e.g., 2025-10-12T00:00:00Z" />
                    </div>
                </div>
                <div class="column">
                    <div class="form-group">
                        <label for="monAlerts_unread">Unread only:</label>
                        <select id="monAlerts_unread">
                            <option value="false">false</option>
                            <option value="true">true</option>
                        </select>
                    </div>
                    <div class="form-group">
                        <label for="monAlerts_limit">Limit:</label>
                        <input id="monAlerts_limit" type="number" value="50" min="1" max="500" />
                    </div>
                </div>
            </div>
            <div class="btn-group">
                <button class="api-button" id="btnMonAlertsList">List Alerts</button>
            </div>
            <div id="monitoringAlerts_list"></div>

            <h3>Recent Alerts (Compact)</h3>
            <div class="columns">
                <div class="column">
                    <div class="form-group">
                        <label for="monAlerts_recent_limit">Limit:</label>
                        <input id="monAlerts_recent_limit" type="number" value="10" min="1" max="100" />
                    </div>
                </div>
                <div class="column">
                    <div class="form-group">
                        <label for="monAlerts_recent_unread">Unread only:</label>
                        <select id="monAlerts_recent_unread"><option value="true">true</option><option value="false">false</option></select>
                    </div>
                </div>
                <div class="column">
                    <div class="btn-group" style="margin-top:24px;">
                        <button class="api-button" id="btnMonAlertsLoadRecent">Load</button>
                    </div>
                </div>
                <div class="column">
                    <div class="form-group">
                        <label for="monAlerts_auto">Auto Refresh:</label>
                        <select id="monAlerts_auto"><option value="false">false</option><option value="true">true</option></select>
                    </div>
                </div>
                <div class="column">
                    <div class="form-group">
                        <label for="monAlerts_auto_interval">Interval (s):</label>
                        <input id="monAlerts_auto_interval" type="number" value="30" min="5" max="300" />
                    </div>
                </div>
                <div class="column">
                    <div class="form-group">
                        <label for="monAlerts_auto_start">Auto Start on Load:</label>
                        <select id="monAlerts_auto_start"><option value="false">false</option><option value="true">true</option></select>
                    </div>
                </div>
                <div class="column">
                    <div class="form-group">
                        <label>Last Update:</label>
                        <span id="monAlerts_last_update">-</span>
                    </div>
                </div>
            </div>
            <div id="monitoringAlerts_recent"></div>
        </div>

        <!-- inline script removed: now handled by js/admin-rbac-monitoring.js -->
        <!--
        <script>
            async function monitoringListWatchlists() {
                try {
                    const res = await apiClient.get('/api/v1/monitoring/watchlists');
                    const listEl = document.getElementById('monitoringWatchlists_list');
                    let wls = (res && res.watchlists) || [];
                    const fScope = (document.getElementById('monWl_filter_scope_type').value || '').trim();
                    const fId = (document.getElementById('monWl_filter_scope_id').value || '').trim();
                    if (fScope) {
                        wls = wls.filter(w => (w.scope_type || '') === fScope);
                    }
                    if (fId) {
                        wls = wls.filter(w => String(w.scope_id || '') === fId);
                    }
                    const showId = document.getElementById('monWl_col_id').checked;
                    const showScope = document.getElementById('monWl_col_scope').checked;
                    const showRules = document.getElementById('monWl_col_rules').checked;
                    let html = '<table class="api-table"><thead><tr>';
                    if (showId) html += '<th>ID</th>';
                    html += '<th>Name</th>';
                    if (showScope) html += '<th>Scope</th>';
                    html += '<th>Enabled</th>';
                    if (showRules) html += '<th>Rules</th>';
                    html += '<th>Actions</th></tr></thead><tbody>';
                    for (const wl of wls) {
                        html += '<tr>';
                        if (showId) html += `<td>${wl.id || ''}</td>`;
                        html += `<td>${wl.name || ''}</td>`;
                        if (showScope) html += `<td>${wl.scope_type || ''}:${wl.scope_id || ''}</td>`;
                        html += `<td>${wl.enabled}</td>`;
                        if (showRules) html += `<td>${(wl.rules||[]).length}</td>`;
                        // Actions column: quick apply defaults to this scope if team/org
                        if (wl.scope_type === 'team' || wl.scope_type === 'org') {
                            html += `<td><button class="btn" onclick="monitoringApplyDefaultsToScope('${wl.scope_type}','${wl.scope_id}')">Apply Defaults to Scope</button></td>`;
                        } else {
                            html += '<td></td>';
                        }
                        html += '</tr>';
                    }
                    html += '</tbody></table>';
                    listEl.innerHTML = html;
                } catch (e) {
                    document.getElementById('monitoringWatchlists_result').textContent = JSON.stringify(e.response || e, null, 2);
                    Toast.error('Failed to list watchlists');
                }
            }

            // Persist watchlist filters and column visibility
            (function () {
                const scopeSel = document.getElementById('monWl_filter_scope_type');
                const scopeId = document.getElementById('monWl_filter_scope_id');
                const colId = document.getElementById('monWl_col_id');
                const colScope = document.getElementById('monWl_col_scope');
                const colRules = document.getElementById('monWl_col_rules');
                const resetBtn = document.getElementById('monWl_filters_reset');
                try {
                    const vScope = localStorage.getItem('monWl_filter_scope_type'); if (vScope !== null) scopeSel.value = vScope;
                    const vScopeId = localStorage.getItem('monWl_filter_scope_id'); if (vScopeId !== null) scopeId.value = vScopeId;
                    const vColId = localStorage.getItem('monWl_col_id'); if (vColId !== null) colId.checked = (vColId === 'true');
                    const vColScope = localStorage.getItem('monWl_col_scope'); if (vColScope !== null) colScope.checked = (vColScope === 'true');
                    const vColRules = localStorage.getItem('monWl_col_rules'); if (vColRules !== null) colRules.checked = (vColRules === 'true');
                } catch (e) {}
                function saveAndRefresh() {
                    try {
                        localStorage.setItem('monWl_filter_scope_type', scopeSel.value);
                        localStorage.setItem('monWl_filter_scope_id', scopeId.value);
                        localStorage.setItem('monWl_col_id', String(colId.checked));
                        localStorage.setItem('monWl_col_scope', String(colScope.checked));
                        localStorage.setItem('monWl_col_rules', String(colRules.checked));
                    } catch (e) {}
                    monitoringListWatchlists();
                }
                function resetAndRefresh() {
                    scopeSel.value = '';
                    scopeId.value = '';
                    colId.checked = true;
                    colScope.checked = true;
                    colRules.checked = true;
                    saveAndRefresh();
                    Toast.info('Filters reset');
                }
                scopeSel.addEventListener('change', saveAndRefresh);
                scopeId.addEventListener('input', saveAndRefresh);
                colId.addEventListener('change', saveAndRefresh);
                colScope.addEventListener('change', saveAndRefresh);
                colRules.addEventListener('change', saveAndRefresh);
                if (resetBtn) resetBtn.addEventListener('click', resetAndRefresh);
                // Initial render reflects persisted prefs
                monitoringListWatchlists();
            })();

            async function monitoringApplyDefaultsToScope(scopeType, scopeId) {
                try {
                    if (!scopeType || !scopeId) { Toast.error('Missing scope'); return; }
                    const listed = await apiClient.get('/api/v1/monitoring/watchlists');
                    const wls = (listed && listed.watchlists) || [];
                    const defaults = wls.filter(w => (w.scope_type === 'global' || w.scope_type === 'all') && ((w.name || '').startsWith('Kid-Safe Defaults')));
                    if (defaults.length === 0) { Toast.error('No default watchlists found'); return; }
                    let created = 0;
                    for (const wl of defaults) {
                        const payload = {
                            id: null,
                            name: `${wl.name} [${scopeType}:${scopeId}]`,
                            description: wl.description || '',
                            enabled: true,
                            scope_type: scopeType,
                            scope_id: scopeId,
                            rules: wl.rules || []
                        };
                        try { await apiClient.post('/api/v1/monitoring/watchlists', payload); created += 1; } catch (e) {}
                    }
                    Toast.success(`Applied ${created} default watchlists to ${scopeType}:${scopeId}`);
                    await monitoringListWatchlists();
                } catch (e) {
                    document.getElementById('monitoringWatchlists_result').textContent = JSON.stringify(e.response || e, null, 2);
                    Toast.error('Failed to apply defaults');
                }
            }

            async function monitoringReloadWatchlists() {
                try {
                    const res = await apiClient.post('/api/v1/monitoring/reload');
                    document.getElementById('monitoringWatchlists_result').textContent = JSON.stringify(res, null, 2);
                    Toast.success('Reloaded');
                    await monitoringListWatchlists();
                } catch (e) {
                    document.getElementById('monitoringWatchlists_result').textContent = JSON.stringify(e.response || e, null, 2);
                    Toast.error('Failed to reload');
                }
            }

            async function monitoringUpsertWatchlist() {
                try {
                    const id = (document.getElementById('monWl_id').value || '').trim() || null;
                    const name = (document.getElementById('monWl_name').value || '').trim();
                    const description = (document.getElementById('monWl_desc').value || '').trim() || null;
                    const enabled = (document.getElementById('monWl_enabled').value === 'true');
                    const scope_type = document.getElementById('monWl_scope_type').value;
                    const scope_id = (document.getElementById('monWl_scope_id').value || '').trim() || '*';
                    const rulesText = document.getElementById('monWl_rules').value || '[]';
                    let rules;
                    try {
                        rules = JSON.parse(rulesText);
                    } catch (e) {
                        Toast.error('Rules must be valid JSON array');
                        return;
                    }
                    const body = { id, name, description, enabled, scope_type, scope_id, rules };
                    const res = await apiClient.post('/api/v1/monitoring/watchlists', body);
                    document.getElementById('monitoringWatchlists_result').textContent = JSON.stringify(res, null, 2);
                    Toast.success('Saved watchlist');
                    await monitoringListWatchlists();
                } catch (e) {
                    document.getElementById('monitoringWatchlists_result').textContent = JSON.stringify(e.response || e, null, 2);
                    Toast.error('Failed to save watchlist');
                }
            }

            async function monitoringDeleteWatchlist() {
                try {
                    const id = (document.getElementById('monWl_id').value || '').trim();
                    if (!id) { Toast.error('Enter watchlist ID to delete'); return; }
                    if (!confirm('Delete watchlist ' + id + '?')) return;
                    const res = await apiClient.delete(`/api/v1/monitoring/watchlists/${encodeURIComponent(id)}`);
                    document.getElementById('monitoringWatchlists_result').textContent = JSON.stringify(res, null, 2);
                    Toast.success('Deleted');
                    await monitoringListWatchlists();
                } catch (e) {
                    document.getElementById('monitoringWatchlists_result').textContent = JSON.stringify(e.response || e, null, 2);
                    Toast.error('Failed to delete watchlist');
                }
            }

            async function monitoringQuickApplyDefaults(scopeType) {
                try {
                    const id = (scopeType === 'team') ? (document.getElementById('monQuick_team').value || '').trim()
                                                      : (document.getElementById('monQuick_org').value || '').trim();
                    if (!id) { Toast.error(`Enter a ${scopeType} id`); return; }
                    const listed = await apiClient.get('/api/v1/monitoring/watchlists');
                    const wls = (listed && listed.watchlists) || [];
                    // Select global defaults (Kid-Safe Defaults - ...)
                    const defaults = wls.filter(w => (w.name || '').startsWith('Kid-Safe Defaults'));
                    if (defaults.length === 0) { Toast.error('No default watchlists found'); return; }
                    let created = 0;
                    for (const wl of defaults) {
                        const payload = {
                            id: null,
                            name: `${wl.name} [${scopeType}:${id}]`,
                            description: wl.description || '',
                            enabled: true,
                            scope_type: scopeType,
                            scope_id: id,
                            rules: wl.rules || []
                        };
                        try {
                            await apiClient.post('/api/v1/monitoring/watchlists', payload);
                            created += 1;
                        } catch (e) {
                            // collect but continue
                        }
                    }
                    Toast.success(`Applied ${created} default watchlists to ${scopeType}:${id}`);
                    await monitoringListWatchlists();
                } catch (e) {
                    document.getElementById('monitoringWatchlists_result').textContent = JSON.stringify(e.response || e, null, 2);
                    Toast.error('Failed to apply defaults');
                }
            }

            async function monitoringBulkApplyDefaults() {
                try {
                    const scopeType = document.getElementById('monBulk_scope').value;
                    const raw = (document.getElementById('monBulk_ids').value || '').trim();
                    if (!raw) { Toast.error('Enter at least one ID'); return; }
                    // Parse IDs by newline and comma
                    const parts = raw.split(/\n|,/).map(s => s.trim()).filter(Boolean);
                    if (parts.length === 0) { Toast.error('No valid IDs found'); return; }
                    const listed = await apiClient.get('/api/v1/monitoring/watchlists');
                    const wls = (listed && listed.watchlists) || [];
                    const defaults = wls.filter(w => (w.scope_type === 'global' || w.scope_type === 'all') && ((w.name || '').startsWith('Kid-Safe Defaults')));
                    if (defaults.length === 0) { Toast.error('No default watchlists found'); return; }
                    let totalCreated = 0;
                    for (const sid of parts) {
                        for (const wl of defaults) {
                            const payload = {
                                id: null,
                                name: `${wl.name} [${scopeType}:${sid}]`,
                                description: wl.description || '',
                                enabled: true,
                                scope_type: scopeType,
                                scope_id: sid,
                                rules: wl.rules || []
                            };
                            try { await apiClient.post('/api/v1/monitoring/watchlists', payload); totalCreated += 1; } catch (e) {}
                        }
                    }
                    Toast.success(`Applied ${totalCreated} watchlists to ${parts.length} ${scopeType} id(s)`);
                    await monitoringListWatchlists();
                } catch (e) {
                    document.getElementById('monitoringWatchlists_result').textContent = JSON.stringify(e.response || e, null, 2);
                    Toast.error('Bulk apply failed');
                }
            }

            async function monitoringListAlerts() {
                try {
                    const user = (document.getElementById('monAlerts_user').value || '').trim() || null;
                    const since = (document.getElementById('monAlerts_since').value || '').trim() || null;
                    const unreadOnly = document.getElementById('monAlerts_unread').value === 'true';
                    const limit = parseInt(document.getElementById('monAlerts_limit').value || '50', 10);
                    const qs = new URLSearchParams();
                    if (user) qs.set('user_id', user);
                    if (since) qs.set('since', since);
                    if (unreadOnly) qs.set('unread_only', 'true');
                    qs.set('limit', String(limit));
                    const res = await apiClient.get(qs.toString() ? `/api/v1/monitoring/alerts?${qs}` : '/api/v1/monitoring/alerts');
                    const items = (res && res.items) || [];
                    let html = '<table class="api-table"><thead><tr><th>ID</th><th>Time</th><th>User</th><th>Source</th><th>Category</th><th>Severity</th><th>Pattern</th><th>Snippet</th><th>Actions</th></tr></thead><tbody>';
                    for (const it of items) {
                        html += `<tr>
                            <td>${Utils.escapeHtml(String(it.id ?? ''))}</td>
                            <td>${Utils.escapeHtml(String(it.created_at ?? ''))}</td>
                            <td>${Utils.escapeHtml(String(it.user_id ?? ''))}</td>
                            <td>${Utils.escapeHtml(String(it.source ?? ''))}</td>
                            <td>${Utils.escapeHtml(String(it.rule_category ?? ''))}</td>
                            <td>${Utils.escapeHtml(String(it.rule_severity ?? ''))}</td>
                            <td>${Utils.escapeHtml(String((it.pattern || '').slice(0,80)))}</td>
                            <td>${Utils.escapeHtml(String((it.text_snippet || '').slice(0,120)))}</td>
                            <td>${it.is_read ? '' : `<button class="btn" onclick="monitoringMarkRead(${it.id})">Mark read</button>`}</td>
                        </tr>`;
                    }
                    html += '</tbody></table>';
                    document.getElementById('monitoringAlerts_list').innerHTML = html;
                } catch (e) {
                    const el = document.getElementById('monitoringAlerts_list');
                    if (el) {
                        el.innerHTML = `<pre>${Utils.escapeHtml(JSON.stringify(e.response || e, null, 2))}</pre>`;
                    }
                    Toast.error('Failed to list alerts');
                }
            }

            async function monitoringMarkRead(id) {
                try {
                    const res = await apiClient.post(`/api/v1/monitoring/alerts/${id}/read`);
                    Toast.success('Marked as read');
                    await monitoringListAlerts();
                } catch (e) {
                    Toast.error('Failed to mark as read');
                }
            }

            async function monitoringLoadRecentAlerts() {
                try {
                    const limit = parseInt(document.getElementById('monAlerts_recent_limit').value || '10', 10);
                    const unreadOnly = document.getElementById('monAlerts_recent_unread').value === 'true';
                    const qs = new URLSearchParams();
                    qs.set('limit', String(limit));
                    if (unreadOnly) qs.set('unread_only', 'true');
                    const res = await apiClient.get(qs.toString() ? `/api/v1/monitoring/alerts?${qs}` : '/api/v1/monitoring/alerts');
                    const items = (res && res.items) || [];
                    let html = '<table class="api-table"><thead><tr><th>Time</th><th>User</th><th>Source</th><th>Category</th><th>Severity</th><th>Snippet</th></tr></thead><tbody>';
                    for (const it of items) {
                        html += `<tr>
                            <td>${it.created_at || ''}</td>
                            <td>${Utils.escapeHtml(String(it.user_id ?? ''))}</td>
                            <td>${it.source || ''}</td>
                            <td>${Utils.escapeHtml(String(it.rule_category ?? ''))}</td>
                            <td>${Utils.escapeHtml(String(it.rule_severity ?? ''))}</td>
                            <td>${(it.text_snippet || '').replace(/</g,'&lt;').slice(0,100)}</td>
                        </tr>`;
                    }
                    html += '</tbody></table>';
                    document.getElementById('monitoringAlerts_recent').innerHTML = html;
                    try {
                        document.getElementById('monAlerts_last_update').textContent = new Date().toLocaleString();
                        const upd = document.getElementById('monAlerts_updated');
                        if (upd) { upd.textContent = 'Updated'; setTimeout(() => { upd.textContent = ''; }, 1500); }
                        if (window._monAlertsTick === 'auto') {
                            const toastSel = document.getElementById('monAlerts_tick_toast');
                            if (toastSel && toastSel.value === 'true') { Toast.info('Alerts updated'); }
                        }
                    } catch (e) {}
                    window._monAlertsTick = null;
                } catch (e) {
                    const el = document.getElementById('monitoringAlerts_recent');
                    if (el) {
                        el.innerHTML = `<pre>${Utils.escapeHtml(JSON.stringify(e.response || e, null, 2))}</pre>`;
                    }
                    Toast.error('Failed to load recent alerts');
                }
            }

            // Auto refresh for alerts
            (function () {
                if (!window._monAlertsTimer) window._monAlertsTimer = null;
                const toggle = document.getElementById('monAlerts_auto');
                const intervalEl = document.getElementById('monAlerts_auto_interval');
                const startSel = document.getElementById('monAlerts_auto_start');
                const toastSel = document.getElementById('monAlerts_tick_toast');
                // Restore persisted
                try {
                    const sAuto = localStorage.getItem('monAlerts_auto'); if (sAuto !== null) toggle.value = sAuto;
                    const sInt = localStorage.getItem('monAlerts_auto_interval'); if (sInt !== null) intervalEl.value = sInt;
                    const sStart = localStorage.getItem('monAlerts_auto_start'); if (sStart !== null) startSel.value = sStart;
                    const sToast = localStorage.getItem('monAlerts_tick_toast'); if (sToast !== null) toastSel.value = sToast;
                } catch (e) {}
                function applyAutoAlerts(runNow=false) {
                    if (window._monAlertsTimer) { clearInterval(window._monAlertsTimer); window._monAlertsTimer = null; }
                    if (toggle.value === 'true') {
                        const ms = Math.max(5000, (parseInt(intervalEl.value || '30', 10) * 1000));
                        window._monAlertsTimer = setInterval(() => { window._monAlertsTick = 'auto'; monitoringLoadRecentAlerts(); }, ms);
                    }
                    if (runNow) { monitoringLoadRecentAlerts(); }
                }
                toggle.addEventListener('change', () => { try { localStorage.setItem('monAlerts_auto', toggle.value); } catch (e) {} applyAutoAlerts(); });
                intervalEl.addEventListener('change', () => { try { localStorage.setItem('monAlerts_auto_interval', intervalEl.value); } catch (e) {} applyAutoAlerts(); });
                startSel.addEventListener('change', () => { try { localStorage.setItem('monAlerts_auto_start', startSel.value); } catch (e) {} });
                toastSel.addEventListener('change', () => { try { localStorage.setItem('monAlerts_tick_toast', toastSel.value); } catch (e) {} });
                if (startSel && startSel.value === 'true') { window._monAlertsTick = 'auto'; applyAutoAlerts(true); }
            })();
        </script>
        -->
        <div class="endpoint-section" id="monitoringNotifications">
            <h2>
                <span class="endpoint-method get">GET/PUT/POST</span>
                <span class="endpoint-path">/api/v1/monitoring/notifications/* - Notification Settings & Test</span>
            </h2>
            <p>Runtime-only notification settings (file/webhook/email). Use env/Config_Files for persistent config.</p>
            <div class="columns">
                <div class="column">
                    <div class="form-group">
                        <label for="monNotif_enabled">Enabled:</label>
                        <select id="monNotif_enabled"><option value="true">true</option><option value="false">false</option></select>
                    </div>
                    <div class="form-group">
                        <label for="monNotif_min_sev">Min Severity:</label>
                        <select id="monNotif_min_sev"><option>critical</option><option>warning</option><option>info</option></select>
                    </div>
                    <div class="form-group">
                        <label for="monNotif_file">File Sink:</label>
                        <input id="monNotif_file" type="text" placeholder="Databases/monitoring_notifications.log" />
                    </div>
                </div>
                <div class="column">
                    <div class="form-group">
                        <label for="monNotif_webhook">Webhook URL:</label>
                        <input id="monNotif_webhook" type="text" placeholder="https://example/hook" />
                    </div>
                    <div class="form-group">
                        <label for="monNotif_email_to">Email To:</label>
                        <input id="monNotif_email_to" type="email" placeholder="owner@example.com" />
                    </div>
                    <details>
                        <summary>SMTP (optional)</summary>
                        <div class="form-group">
                            <label for="monNotif_smtp_host">SMTP Host:</label>
                            <input id="monNotif_smtp_host" type="text" placeholder="smtp.example.com" />
                        </div>
                        <div class="form-group">
                            <label for="monNotif_smtp_port">SMTP Port:</label>
                            <input id="monNotif_smtp_port" type="number" placeholder="587" />
                        </div>
                        <div class="form-group">
                            <label for="monNotif_smtp_starttls">STARTTLS:</label>
                            <select id="monNotif_smtp_starttls"><option value="true">true</option><option value="false">false</option></select>
                        </div>
                        <div class="form-group">
                            <label for="monNotif_smtp_user">SMTP User:</label>
                            <input id="monNotif_smtp_user" type="text" placeholder="user@example.com" />
                        </div>
                        <div class="form-group">
                            <label for="monNotif_smtp_pass">SMTP Password:</label>
                            <input id="monNotif_smtp_pass" type="password" placeholder="••••••" />
                        </div>
                        <div class="form-group">
                            <label for="monNotif_email_from">From Address:</label>
                            <input id="monNotif_email_from" type="email" placeholder="alerts@example.com" />
                        </div>
                    </details>
                </div>
            </div>
            <div class="btn-group">
                <button class="api-button" id="btnMonNotifLoad">Load</button>
                <button class="api-button" id="btnMonNotifSave">Save</button>
                <button class="api-button subtle" id="btnMonNotifClearDrafts" title="Remove locally saved draft values">Clear Drafts</button>
                <button class="api-button subtle" id="btnMonNotifRestoreDefaults" title="Reload from server and overwrite drafts">Restore Defaults</button>
            </div>
            <h3>Test Notification</h3>
            <div class="columns">
                <div class="column">
                    <div class="form-group">
                        <label for="monNotif_test_sev">Severity:</label>
                        <select id="monNotif_test_sev"><option>critical</option><option>warning</option><option>info</option></select>
                    </div>
                </div>
                <div class="column">
                    <div class="form-group">
                        <label for="monNotif_test_msg">Message:</label>
                        <input id="monNotif_test_msg" type="text" placeholder="Test notification" />
                    </div>
                </div>
            </div>
            <div class="btn-group">
                <button class="api-button" id="btnMonNotifSendTest">Send Test</button>
            </div>
            <pre id="monitoringNotif_result">---</pre>
            <h3>Recent Notifications</h3>
            <div class="columns">
                <div class="column">
                    <div class="form-group">
                        <label for="monNotif_recent_limit">Limit:</label>
                        <input id="monNotif_recent_limit" type="number" value="50" min="1" max="500" />
                    </div>
                </div>
                <div class="column">
                    <div class="btn-group" style="margin-top: 24px;">
                        <button class="api-button" id="btnMonNotifLoadRecent">Load Recent</button>
                    </div>
                </div>
                <div class="column">
                    <div class="form-group">
                        <label for="monNotif_auto">Auto Refresh:</label>
                        <select id="monNotif_auto"><option value="false">false</option><option value="true">true</option></select>
                    </div>
                </div>
                <div class="column">
                    <div class="form-group">
                        <label for="monNotif_auto_interval">Interval (s):</label>
                        <input id="monNotif_auto_interval" type="number" value="30" min="5" max="300" />
                    </div>
                </div>
                <div class="column">
                    <div class="form-group">
                        <label for="monNotif_auto_start">Auto Start on Load:</label>
                        <select id="monNotif_auto_start"><option value="false">false</option><option value="true">true</option></select>
                    </div>
                </div>
                <div class="column">
                    <div class="form-group">
                        <label>Last Update:</label>
                        <span id="monNotif_last_update">-</span>
                        <span id="monNotif_updated" style="margin-left:8px;color:#28a745;"></span>
                    </div>
                </div>
                <div class="column">
                    <div class="form-group">
                        <label for="monNotif_tick_toast">Tick Toast:</label>
                        <select id="monNotif_tick_toast"><option value="false">false</option><option value="true">true</option></select>
                    </div>
                </div>
            </div>
            <div id="monitoringNotif_recent"></div>
        </div>

        <!-- inline script removed: now handled by js/admin-rbac-monitoring.js -->
        <!--
        <script>
        async function monitoringLoadNotifSettings() {
            try {
                const s = await apiClient.get('/api/v1/monitoring/notifications/settings');
                document.getElementById('monNotif_enabled').value = String(!!s.enabled);
                document.getElementById('monNotif_min_sev').value = s.min_severity || 'critical';
                document.getElementById('monNotif_file').value = s.file || '';
                document.getElementById('monNotif_webhook').value = s.webhook_url || '';
                document.getElementById('monNotif_email_to').value = s.email_to || '';
                document.getElementById('monNotif_smtp_host').value = s.smtp_host || '';
                document.getElementById('monNotif_smtp_port').value = s.smtp_port || 587;
                document.getElementById('monNotif_smtp_starttls').value = String(!!s.smtp_starttls);
                document.getElementById('monNotif_smtp_user').value = s.smtp_user || '';
                document.getElementById('monNotif_email_from').value = s.email_from || '';
                document.getElementById('monitoringNotif_result').textContent = JSON.stringify(s, null, 2);
                // Persist loaded values as draft
                try {
                    localStorage.setItem('monNotif_enabled', String(!!s.enabled));
                    localStorage.setItem('monNotif_min_sev', s.min_severity || 'critical');
                    localStorage.setItem('monNotif_file', s.file || '');
                    localStorage.setItem('monNotif_webhook', s.webhook_url || '');
                    localStorage.setItem('monNotif_email_to', s.email_to || '');
                    localStorage.setItem('monNotif_smtp_host', s.smtp_host || '');
                    localStorage.setItem('monNotif_smtp_port', String(s.smtp_port || 587));
                    localStorage.setItem('monNotif_smtp_starttls', String(!!s.smtp_starttls));
                    localStorage.setItem('monNotif_smtp_user', s.smtp_user || '');
                    localStorage.setItem('monNotif_email_from', s.email_from || '');
                } catch (e) {}
                Toast.success('Loaded notification settings');
            } catch (e) {
                document.getElementById('monitoringNotif_result').textContent = JSON.stringify(e.response || e, null, 2);
                Toast.error('Failed to load notification settings');
            }
        }

            async function monitoringSaveNotifSettings() {
                try {
                    const body = {
                        enabled: document.getElementById('monNotif_enabled').value === 'true',
                        min_severity: document.getElementById('monNotif_min_sev').value,
                        file: (document.getElementById('monNotif_file').value || '').trim(),
                        webhook_url: (document.getElementById('monNotif_webhook').value || '').trim(),
                        email_to: (document.getElementById('monNotif_email_to').value || '').trim(),
                        smtp_host: (document.getElementById('monNotif_smtp_host').value || '').trim(),
                        smtp_port: parseInt(document.getElementById('monNotif_smtp_port').value || '587', 10),
                        smtp_starttls: document.getElementById('monNotif_smtp_starttls').value === 'true',
                        smtp_user: (document.getElementById('monNotif_smtp_user').value || '').trim(),
                        smtp_password: (document.getElementById('monNotif_smtp_pass').value || '').trim(),
                        email_from: (document.getElementById('monNotif_email_from').value || '').trim(),
                    };
                    const s = await apiClient.put('/api/v1/monitoring/notifications/settings', body);
                    document.getElementById('monitoringNotif_result').textContent = JSON.stringify(s, null, 2);
                    // Persist saved draft values
                    try {
                        localStorage.setItem('monNotif_enabled', document.getElementById('monNotif_enabled').value);
                        localStorage.setItem('monNotif_min_sev', document.getElementById('monNotif_min_sev').value);
                        localStorage.setItem('monNotif_file', (document.getElementById('monNotif_file').value || '').trim());
                        localStorage.setItem('monNotif_webhook', (document.getElementById('monNotif_webhook').value || '').trim());
                        localStorage.setItem('monNotif_email_to', (document.getElementById('monNotif_email_to').value || '').trim());
                        localStorage.setItem('monNotif_smtp_host', (document.getElementById('monNotif_smtp_host').value || '').trim());
                        localStorage.setItem('monNotif_smtp_port', String(parseInt(document.getElementById('monNotif_smtp_port').value || '587', 10)));
                        localStorage.setItem('monNotif_smtp_starttls', document.getElementById('monNotif_smtp_starttls').value);
                        localStorage.setItem('monNotif_smtp_user', (document.getElementById('monNotif_smtp_user').value || '').trim());
                        localStorage.setItem('monNotif_email_from', (document.getElementById('monNotif_email_from').value || '').trim());
                    } catch (e) {}
                    Toast.success('Saved (runtime) notification settings');
                } catch (e) {
                    document.getElementById('monitoringNotif_result').textContent = JSON.stringify(e.response || e, null, 2);
                    Toast.error('Failed to save settings');
                }
            }

            function monitoringClearNotifDrafts() {
                if (!confirm('Clear locally saved notification drafts?')) { return; }
                try {
                    const ids = ['monNotif_enabled','monNotif_min_sev','monNotif_file','monNotif_webhook','monNotif_email_to','monNotif_smtp_host','monNotif_smtp_port','monNotif_smtp_starttls','monNotif_smtp_user','monNotif_email_from'];
                    for (const id of ids) { localStorage.removeItem(id); }
                    // Reset form to blanks/defaults (does not touch password)
                    document.getElementById('monNotif_enabled').value = 'false';
                    document.getElementById('monNotif_min_sev').value = 'critical';
                    document.getElementById('monNotif_file').value = '';
                    document.getElementById('monNotif_webhook').value = '';
                    document.getElementById('monNotif_email_to').value = '';
                    document.getElementById('monNotif_smtp_host').value = '';
                    document.getElementById('monNotif_smtp_port').value = 587;
                    document.getElementById('monNotif_smtp_starttls').value = 'true';
                    document.getElementById('monNotif_smtp_user').value = '';
                    document.getElementById('monNotif_email_from').value = '';
                    Toast.info('Notification drafts cleared');
                } catch (e) {
                    Toast.error('Failed to clear drafts');
                }
            }

            async function monitoringRestoreNotifDefaults() {
                if (!confirm('Reload notification settings from server and overwrite drafts?')) { return; }
                try {
                    await monitoringLoadNotifSettings();
                    Toast.success('Restored notification settings from server');
                } catch (e) {
                    Toast.error('Failed to restore settings');
                }
            }

            function monitoringResetAllMonitoringUI() {
                if (!confirm('Reset all Monitoring UI preferences (watchlist filters/columns + notification drafts)?')) { return; }
                try {
                    // Clear watchlist filter prefs
                    const wlKeys = ['monWl_filter_scope_type','monWl_filter_scope_id','monWl_col_id','monWl_col_scope','monWl_col_rules'];
                    for (const k of wlKeys) localStorage.removeItem(k);
                    // Reset filter controls
                    try {
                        document.getElementById('monWl_filter_scope_type').value = '';
                        document.getElementById('monWl_filter_scope_id').value = '';
                        document.getElementById('monWl_col_id').checked = true;
                        document.getElementById('monWl_col_scope').checked = true;
                        document.getElementById('monWl_col_rules').checked = true;
                        monitoringListWatchlists();
                    } catch (e) {}
                    // Clear notification drafts (reuse helper without confirm)
                    try {
                        const ids = ['monNotif_enabled','monNotif_min_sev','monNotif_file','monNotif_webhook','monNotif_email_to','monNotif_smtp_host','monNotif_smtp_port','monNotif_smtp_starttls','monNotif_smtp_user','monNotif_email_from'];
                        for (const id of ids) { localStorage.removeItem(id); }
                        document.getElementById('monNotif_enabled').value = 'false';
                        document.getElementById('monNotif_min_sev').value = 'critical';
                        document.getElementById('monNotif_file').value = '';
                        document.getElementById('monNotif_webhook').value = '';
                        document.getElementById('monNotif_email_to').value = '';
                        document.getElementById('monNotif_smtp_host').value = '';
                        document.getElementById('monNotif_smtp_port').value = 587;
                        document.getElementById('monNotif_smtp_starttls').value = 'true';
                        document.getElementById('monNotif_smtp_user').value = '';
                        document.getElementById('monNotif_email_from').value = '';
                    } catch (e) {}
                    Toast.success('Monitoring UI preferences reset');
                } catch (e) {
                    Toast.error('Failed to reset UI');
                }
            }

            async function monitoringSendNotifTest() {
                try {
                    const body = {
                        severity: document.getElementById('monNotif_test_sev').value,
                        message: document.getElementById('monNotif_test_msg').value || 'Test notification from admin panel',
                    };
                    const res = await apiClient.post('/api/v1/monitoring/notifications/test', body);
                    document.getElementById('monitoringNotif_result').textContent = JSON.stringify(res, null, 2);
                    Toast.success('Test sent (check file/webhook/email)');
                } catch (e) {
                    document.getElementById('monitoringNotif_result').textContent = JSON.stringify(e.response || e, null, 2);
                    Toast.error('Failed to send test');
                }
            }

            async function monitoringLoadRecentNotifications() {
                try {
                    const limit = parseInt(document.getElementById('monNotif_recent_limit').value || '50', 10);
                    const res = await apiClient.get(`/api/v1/monitoring/notifications/recent?limit=${limit}`);
                    const items = (res && res.items) || [];
                    let html = '<table class="api-table"><thead><tr><th>Time</th><th>Source</th><th>User</th><th>Category</th><th>Severity</th><th>Snippet</th></tr></thead><tbody>';
                    for (const it of items) {
                        const ts = Utils.escapeHtml(String(it.ts || ''));
                        const src = Utils.escapeHtml(String(it.source || ''));
                        const uid = Utils.escapeHtml(String(it.user_id ?? ''));
                        const cat = Utils.escapeHtml(String(it.rule_category ?? ''));
                        const sev = Utils.escapeHtml(String(it.rule_severity ?? ''));
                        const snip = Utils.escapeHtml(String((it.snippet || '').slice(0,140)));
                        html += `<tr>
                            <td>${ts}</td>
                            <td>${src}</td>
                            <td>${uid}</td>
                            <td>${cat}</td>
                            <td>${sev}</td>
                            <td>${snip}</td>
                        </tr>`;
                    }
                    html += '</tbody></table>';
                    document.getElementById('monitoringNotif_recent').innerHTML = html;
                    try {
                        document.getElementById('monNotif_last_update').textContent = new Date().toLocaleString();
                        const upd = document.getElementById('monNotif_updated');
                        if (upd) { upd.textContent = 'Updated'; setTimeout(() => { upd.textContent = ''; }, 1500); }
                        // Auto-tick toast
                        if (window._monNotifTick === 'auto') {
                            const toastSel = document.getElementById('monNotif_tick_toast');
                            if (toastSel && toastSel.value === 'true') {
                                Toast.info('Notifications updated');
                            }
                        }
                    } catch (e) {}
                    window._monNotifTick = null;
                } catch (e) {
                    const el = document.getElementById('monitoringNotif_recent');
                    if (el) {
                        el.innerHTML = `<pre>${Utils.escapeHtml(JSON.stringify(e.response || e, null, 2))}</pre>`;
                    }
                    Toast.error('Failed to load recent notifications');
                }
            }

            // Auto refresh for notifications + persist form drafts
            (function () {
                if (!window._monNotifTimer) window._monNotifTimer = null;
                const toggle = document.getElementById('monNotif_auto');
                const intervalEl = document.getElementById('monNotif_auto_interval');
                const startSel = document.getElementById('monNotif_auto_start');
                const toastSel = document.getElementById('monNotif_tick_toast');
                // Restore persisted settings
                try {
                    const sAuto = localStorage.getItem('monNotif_auto'); if (sAuto !== null) toggle.value = sAuto;
                    const sInt = localStorage.getItem('monNotif_auto_interval'); if (sInt !== null) intervalEl.value = sInt;
                    const sStart = localStorage.getItem('monNotif_auto_start'); if (sStart !== null) startSel.value = sStart;
                    const sToast = localStorage.getItem('monNotif_tick_toast'); if (sToast !== null) toastSel.value = sToast;
                } catch (e) {}
                // Restore draft values for notification settings form
                try {
                    const ids = ['monNotif_enabled','monNotif_min_sev','monNotif_file','monNotif_webhook','monNotif_email_to','monNotif_smtp_host','monNotif_smtp_port','monNotif_smtp_starttls','monNotif_smtp_user','monNotif_email_from'];
                    for (const id of ids) {
                        const el = document.getElementById(id);
                        const v = localStorage.getItem(id);
                        if (el && v !== null) { el.value = v; }
                    }
                } catch (e) {}
                // Save draft on changes (excluding password)
                (function attachDraftSavers() {
                    const ids = ['monNotif_enabled','monNotif_min_sev','monNotif_file','monNotif_webhook','monNotif_email_to','monNotif_smtp_host','monNotif_smtp_port','monNotif_smtp_starttls','monNotif_smtp_user','monNotif_email_from'];
                    for (const id of ids) {
                        const el = document.getElementById(id);
                        if (!el) continue;
                        const ev = (el.tagName === 'INPUT' && el.type === 'text') || (el.tagName === 'INPUT' && el.type === 'email') || (el.tagName === 'INPUT' && el.type === 'number') ? 'input' : 'change';
                        el.addEventListener(ev, () => { try { localStorage.setItem(id, el.value); } catch (e) {} });
                    }
                })();
                function applyAutoNotif(runNow=false) {
                    if (window._monNotifTimer) { clearInterval(window._monNotifTimer); window._monNotifTimer = null; }
                    if (toggle.value === 'true') {
                        const ms = Math.max(5000, (parseInt(intervalEl.value || '30', 10) * 1000));
                        window._monNotifTimer = setInterval(() => { window._monNotifTick = 'auto'; monitoringLoadRecentNotifications(); }, ms);
                    }
                    if (runNow) { monitoringLoadRecentNotifications(); }
                }
                toggle.addEventListener('change', () => { try { localStorage.setItem('monNotif_auto', toggle.value); } catch (e) {} applyAutoNotif(); });
                intervalEl.addEventListener('change', () => { try { localStorage.setItem('monNotif_auto_interval', intervalEl.value); } catch (e) {} applyAutoNotif(); });
                startSel.addEventListener('change', () => { try { localStorage.setItem('monNotif_auto_start', startSel.value); } catch (e) {} });
                toastSel.addEventListener('change', () => { try { localStorage.setItem('monNotif_tick_toast', toastSel.value); } catch (e) {} });
                if (startSel && startSel.value === 'true') {
                    window._monNotifTick = 'auto';
                    applyAutoNotif(true);
                }
            })();
        </script>
        -->
    </div>

    <!-- LLM Usage Charts -->
    <div class="endpoint-section" id="adminLLMUsageCharts">
        <h2>LLM Usage Charts</h2>
        <p>Visual summaries for top spenders and model mix.</p>

        <div class="columns">
            <div class="column">
                <div class="form-group">
                    <label for="adminLLMCharts_start">Start (YYYY-MM-DD):</label>
                    <input type="date" id="adminLLMCharts_start" placeholder="2025-01-01">
                </div>
            </div>
            <div class="column">
                <div class="form-group">
                    <label for="adminLLMCharts_end">End (YYYY-MM-DD):</label>
                    <input type="date" id="adminLLMCharts_end" placeholder="2025-01-31">
                </div>
            </div>
            <div class="column">
                <div class="form-group">
                    <label for="llmCharts_topN">Top N:</label>
                    <input type="number" id="llmCharts_topN" value="10" min="3" max="50">
                </div>
            </div>
            <div class="column">
                <div class="form-group">
                    <label for="llmCharts_model_metric">Model Metric:</label>
                    <select id="llmCharts_model_metric">
                        <option value="tokens" selected>Tokens</option>
                        <option value="cost">Cost</option>
                    </select>
                </div>
            </div>
            <div class="column">
                <div class="form-group">
                    <label for="llmCharts_provider_metric">Provider Metric:</label>
                    <select id="llmCharts_provider_metric">
                        <option value="tokens">Tokens</option>
                        <option value="cost" selected>Cost</option>
                    </select>
                </div>
            </div>
            <div class="column">
                <div class="form-group">
                    <label for="llmCharts_model_palette">Model Palette:</label>
                    <select id="llmCharts_model_palette">
                        <option value="distinct" selected>Distinct</option>
                        <option value="provider">By Provider</option>
                    </select>
                </div>
            </div>
        </div>

        <div class="btn-group">
            <button class="api-button" id="btnAdminLoadLLMCharts" data-requires-api="true">Load Charts</button>
            <button class="btn btn-secondary btn-sm" id="btnResetLegendTop" title="Reset Top Spenders visibility">Reset Top</button>
            <button class="btn btn-secondary btn-sm" id="btnResetLegendModel" title="Reset Model Mix visibility">Reset Model</button>
            <button class="btn btn-secondary btn-sm" id="btnResetLegendProvider" title="Reset Provider Mix visibility">Reset Provider</button>
        </div>

        <div class="columns mt-3">
            <div class="column">
                <h3>Top Spenders</h3>
                <div id="llmChartTopSpenders" class="chart-container"></div>
                <div id="llmLegendTopSpenders" class="chart-legend"></div>
            </div>
            <div class="column">
                <h3>Model Mix</h3>
                <div id="llmChartModelMix" class="chart-container"></div>
                <div id="llmLegendModelMix" class="chart-legend"></div>
            </div>
            <div class="column">
                <h3>Provider Mix</h3>
                <div id="llmChartProviderMix" class="chart-container"></div>
                <div id="llmLegendProviderMix" class="chart-legend"></div>
            </div>
        </div>
    </div>

    <!-- Charts logic moved to js/admin-advanced.js -->

    <!-- Legend toggles and Registration Codes logic moved to js/admin-advanced.js -->

    <div id="tabModeration" class="tab-content">
    <!-- Moderation Management -->
    <div class="endpoint-section" id="moderationSettings">
        <h2>
            <span class="endpoint-method get">GET/PUT</span>
            <span class="endpoint-path">/api/v1/moderation/settings - Runtime Settings</span>
        </h2>
        <p>Quick toggles for moderation categories and built-in PII rules (non-persistent across restarts).</p>
        <div class="columns">
            <div class="column">
                <div class="form-group">
                    <label for="modSettings_pii">Enable built-in PII rules:</label>
                    <select id="modSettings_pii">
                        <option value="">(no override)</option>
                        <option value="true">true</option>
                        <option value="false">false</option>
                    </select>
                </div>
            </div>
            <div class="column">
                <div class="form-group">
                    <label for="modSettings_categories">Categories enabled (comma-separated):</label>
                    <input id="modSettings_categories" type="text" placeholder="e.g., pii,confidential" />
                </div>
            </div>
        </div>
        <div class="btn-group">
            <button class="api-button" id="btnModSettingsLoad">Load</button>
            <button class="api-button" id="btnModSettingsSave">Save</button>
            <label style="margin-left:12px;">
                <input type="checkbox" id="modSettings_persist" /> Persist to file
            </label>
        </div>
        <h3>Status:</h3>
        <pre id="moderationSettings_status">---</pre>
    </div>

    <!-- inline script removed: now handled by js/admin-advanced.js -->
    <!--
    <script>
        async function moderationLoadSettings() {
            try {
                const res = await apiClient.get('/api/v1/moderation/settings');
                // Show effective values
                const eff = res && res.effective ? res.effective : {};
                document.getElementById('modSettings_categories').value = (eff.categories_enabled || []).join(',');
                const piiOverride = res && Object.prototype.hasOwnProperty.call(res, 'pii_enabled') ? res.pii_enabled : null;
                document.getElementById('modSettings_pii').value = (piiOverride === null || piiOverride === undefined) ? '' : String(!!piiOverride);
                document.getElementById('moderationSettings_status').textContent = JSON.stringify(res, null, 2);
                Toast.success('Loaded settings');
            } catch (e) {
                document.getElementById('moderationSettings_status').textContent = JSON.stringify(e.response || e, null, 2);
                Toast.error('Failed to load settings');
            }
        }

        async function moderationSaveSettings() {
            try {
                const rawCats = (document.getElementById('modSettings_categories').value || '').trim();
                const cats = rawCats ? rawCats.split(',').map(x => x.trim()).filter(Boolean) : [];
                const piiVal = document.getElementById('modSettings_pii').value;
                const body = {};
                if (piiVal !== '') body.pii_enabled = (piiVal === 'true');
                body.categories_enabled = cats;
                body.persist = !!document.getElementById('modSettings_persist').checked;
                const res = await apiClient.put('/api/v1/moderation/settings', body);
                document.getElementById('moderationSettings_status').textContent = JSON.stringify(res, null, 2);
                Toast.success('Saved settings');
            } catch (e) {
                document.getElementById('moderationSettings_status').textContent = JSON.stringify(e.response || e, null, 2);
                Toast.error('Failed to save settings');
            }
        }
    </script>
    -->
    <div class="endpoint-section" id="moderationBlocklistManaged">
        <h2>
            <span class="endpoint-method get">GET/POST/DELETE</span>
            <span class="endpoint-path">/api/v1/moderation/blocklist/managed - Managed Blocklist</span>
        </h2>
        <p>List, append, and remove blocklist entries with concurrency protection. Uses a content hash version as ETag.</p>
        <p style="font-size: 0.9em; color: #666; margin-top: -6px;">
            Tip: Managed operations require sending the current ETag in an <code>If-Match</code> header.
            Click <strong>Load</strong> to refresh and keep the ETag up to date. If someone else updates the list first,
            your append/delete will return <code>412 Precondition Failed</code>-just reload and try again.
        </p>

        <div class="columns">
            <div class="column">
                <div class="form-group">
                    <label for="moderationManaged_filter">Filter:</label>
                    <input id="moderationManaged_filter" type="text" placeholder="Search patterns..." />
                    <div style="margin-top:6px;">
                        <label style="font-size:0.9em; color:#666;">
                            <input id="moderationManaged_onlyInvalid" type="checkbox" />
                            Only invalid
                        </label>
                    </div>
                </div>
            </div>
            <div class="column">
                <div class="form-group">
                    <label for="moderationManaged_newLine">Append Line:</label>
                    <input id="moderationManaged_newLine" type="text" placeholder="/secret\\d+/ or literal" />
                </div>
                <div class="btn-group">
                    <button class="api-button" id="btnModerationAppendManaged">Append</button>
                    <button class="api-button" id="btnModerationLintManaged">Lint</button>
                </div>
            </div>
        </div>

        <div class="btn-group">
            <button class="api-button" id="btnModerationLoadManaged">Load</button>
            <button class="api-button" id="btnModerationRefreshManaged">Refresh</button>
        </div>

        <div id="moderationManaged_table"></div>
        <div id="moderationManaged_legend" style="font-size: 0.9em; color: #666; margin-top: 6px;">
            Legend: <span class="lint-ok"><span class="lint-icon">✓</span>ok</span>,
            <span class="lint-invalid"><span class="lint-icon">⚠</span>invalid</span>. Label shows pattern type (literal/regex/comment/empty).
        </div>
        <h3>Status:</h3>
        <pre id="moderationManaged_status">---</pre>
    </div>

    <!-- inline script removed: now handled by js/admin-advanced.js -->
    <!--
    <script>
        window._moderationManaged = { version: '', items: [] };
        window._moderationManagedLint = {}; // id -> lint item

        function renderManagedBlocklist() {
            const container = document.getElementById('moderationManaged_table');
            const filter = (document.getElementById('moderationManaged_filter').value || '').toLowerCase();
            let items = (window._moderationManaged.items || []).filter(it => !filter || String(it.line).toLowerCase().includes(filter));
            const onlyInvalid = !!document.getElementById('moderationManaged_onlyInvalid')?.checked;
            if (onlyInvalid) {
                items = items.filter((it) => {
                    const lint = window._moderationManagedLint[String(it.id)] || null;
                    return lint && lint.ok === false;
                });
            }
            let html = '<table class="table"><thead><tr><th>ID</th><th>Pattern</th><th>Lint</th><th>Actions</th></tr></thead><tbody>';
            for (const it of items) {
                const lint = window._moderationManagedLint[String(it.id)] || null;
                const lintText = lint ? (lint.ok ? 'ok' : (lint.error || 'invalid')) : '';
                const lintClass = lint ? (lint.ok ? 'ok' : 'invalid') : '';
                const lintIcon = lint ? (lint.ok ? '✓' : '⚠') : '';
                html += `<tr>
                    <td>${Utils.escapeHtml(String(it.id ?? ''))}</td>
                    <td><code>${Utils.escapeHtml(String(it.line))}</code></td>
                    <td><span class="lint-${lintClass}" title="${Utils.escapeHtml(lintText)}"><span class="lint-icon">${lintIcon}</span>${Utils.escapeHtml(lint ? (lint.pattern_type || '') : '')}</span></td>
                    <td><button class="btn btn-danger" onclick="moderationDeleteManaged(${it.id})">Delete</button></td>
                </tr>`;
            }
            html += '</tbody></table>';
            container.innerHTML = html;
        }

        async function moderationLoadManaged() {
            try {
                const res = await apiClient.get('/api/v1/moderation/blocklist/managed');
                window._moderationManaged = res || { version: '', items: [] };
                await moderationLintManagedAll();
                renderManagedBlocklist();
                document.getElementById('moderationManaged_status').textContent = `Loaded version: ${res.version}`;
                Toast.success('Loaded managed blocklist');
            } catch (e) {
                document.getElementById('moderationManaged_status').textContent = JSON.stringify(e.response || e, null, 2);
                Toast.error('Failed to load managed blocklist');
            }
        }

        async function moderationRefreshManaged() { return moderationLoadManaged(); }

        async function moderationAppendManaged() {
            try {
                const line = (document.getElementById('moderationManaged_newLine').value || '').trim();
                if (!line) { Toast.error('Enter a line'); return; }
                // Automatic lint before append
                const lint = await apiClient.post('/api/v1/moderation/blocklist/lint', { line });
                const invalid = (lint.items || []).filter(it => !it.ok);
                if (invalid.length > 0) {
                    document.getElementById('moderationManaged_status').textContent = JSON.stringify(lint, null, 2);
                    Toast.error('Lint failed: fix the line before append');
                    return;
                }
                const res = await apiClient.post('/api/v1/moderation/blocklist/append', { line }, { headers: { 'If-Match': window._moderationManaged.version }});
                window._moderationManaged.version = res.version;
                await moderationLoadManaged();
                document.getElementById('moderationManaged_newLine').value = '';
                Toast.success('Appended');
            } catch (e) {
                document.getElementById('moderationManaged_status').textContent = JSON.stringify(e.response || e, null, 2);
                Toast.error('Failed to append');
            }
        }

        async function moderationDeleteManaged(id) {
            try {
                if (!confirm('Delete blocklist entry #' + id + '?')) return;
                const res = await apiClient.delete(`/api/v1/moderation/blocklist/${id}`, { headers: { 'If-Match': window._moderationManaged.version }});
                window._moderationManaged.version = res.version;
                await moderationLoadManaged();
                Toast.success('Deleted');
            } catch (e) {
                document.getElementById('moderationManaged_status').textContent = JSON.stringify(e.response || e, null, 2);
                Toast.error('Failed to delete');
            }
        }

        async function moderationLintManaged() {
            try {
                const line = (document.getElementById('moderationManaged_newLine').value || '').trim();
                if (!line) { Toast.error('Enter a line'); return; }
                const res = await apiClient.post('/api/v1/moderation/blocklist/lint', { line });
                const invalid = (res.items || []).filter(it => !it.ok);
                const msg = `Lint: ${res.valid_count} valid, ${res.invalid_count} invalid`;
                document.getElementById('moderationManaged_status').textContent = JSON.stringify(res, null, 2);
                if (invalid.length === 0) {
                    Toast.success(msg);
                } else {
                    Toast.error(msg);
                }
            } catch (e) {
                document.getElementById('moderationManaged_status').textContent = JSON.stringify(e.response || e, null, 2);
                Toast.error('Lint failed');
            }
        }

        async function moderationLintManagedAll() {
            try {
                const lines = (window._moderationManaged.items || []).map(it => it.line);
                if (!lines.length) { window._moderationManagedLint = {}; return; }
                const res = await apiClient.post('/api/v1/moderation/blocklist/lint', { lines });
                const map = {};
                // Map by sequential index (same order as items), which equals the managed id
                (res.items || []).forEach((it, i) => { map[String(i)] = it; });
                window._moderationManagedLint = map;
            } catch (e) {
                // Non-fatal; keep table rendering
                window._moderationManagedLint = {};
            }
        }
    </script>
    -->
    <div class="endpoint-section" id="moderationBlocklist">
        <h2>
            <span class="endpoint-method get">GET/PUT</span>
            <span class="endpoint-path">/api/v1/moderation/blocklist - Blocklist</span>
        </h2>
        <p>View and update the global moderation blocklist. Each line is a literal substring or a regex when wrapped in slashes (e.g., <code>/secret\d+/</code>).</p>

        <div class="form-group">
            <label for="moderationBlocklist_text">Blocklist:</label>
            <textarea id="moderationBlocklist_text" rows="10" placeholder="# One pattern per line"></textarea>
        </div>

        <div class="btn-group">
            <button class="api-button" id="btnModerationLoadBlocklist">Load</button>
            <button class="api-button" id="btnModerationLintBlocklist">Lint</button>
            <button class="api-button" id="btnModerationSaveBlocklist">Save</button>
        </div>

        <div id="moderationBlocklist_legend" style="font-size: 0.9em; color: #666; margin-top: 6px;">
            Legend: <span class="lint-ok"><span class="lint-icon">✓</span>ok</span>,
            <span class="lint-invalid"><span class="lint-icon">⚠</span>invalid</span>. Lint results are shown in the Status box.
        </div>

        <div style="margin-top:6px;">
            <label style="font-size:0.9em; color:#666;">
                <input id="moderationBlocklist_onlyInvalid" type="checkbox" />
                Only invalid (from last lint)
            </label>
        </div>

        <h3>Status:</h3>
        <pre id="moderationBlocklist_status">---</pre>
        <div id="moderationBlocklist_invalidList" style="margin-top:8px;"></div>
        <div id="moderationBlocklist_invalidActions" class="btn-group" style="display:none; margin-top:6px;">
            <button class="btn btn-secondary" id="btnModerationCopyInvalidBlocklist">Copy invalid lines</button>
        </div>
    </div>

    <!-- inline script removed: now handled by js/admin-advanced.js -->
    <!--
    <script>
        async function moderationLoadBlocklist() {
            try {
                const lines = await apiClient.get('/api/v1/moderation/blocklist');
                document.getElementById('moderationBlocklist_text').value = (lines || []).join('\n');
                document.getElementById('moderationBlocklist_status').textContent = 'Loaded';
                Toast.success('Loaded blocklist');
            } catch (e) {
                document.getElementById('moderationBlocklist_status').textContent = JSON.stringify(e.response || e, null, 2);
                Toast.error('Failed to load blocklist');
            }
        }

        async function moderationSaveBlocklist() {
            try {
                const raw = document.getElementById('moderationBlocklist_text').value || '';
                const lines = raw.split(/\r?\n/);
                const res = await apiClient.put('/api/v1/moderation/blocklist', { lines });
                document.getElementById('moderationBlocklist_status').textContent = JSON.stringify(res, null, 2);
                Toast.success('Blocklist saved');
            } catch (e) {
                document.getElementById('moderationBlocklist_status').textContent = JSON.stringify(e.response || e, null, 2);
                Toast.error('Failed to save blocklist');
            }
        }

        // Store last lint response for raw blocklist
        window._moderationBlocklistLastLint = null;

        async function moderationLintBlocklist() {
            try {
                const raw = document.getElementById('moderationBlocklist_text').value || '';
                const lines = raw.split(/\r?\n/);
                const res = await apiClient.post('/api/v1/moderation/blocklist/lint', { lines });
                const invalid = (res.items || []).filter(it => !it.ok);
                const msg = `Lint: ${res.valid_count} valid, ${res.invalid_count} invalid`;
                document.getElementById('moderationBlocklist_status').textContent = JSON.stringify(res, null, 2);
                window._moderationBlocklistLastLint = res;
                renderBlocklistInvalidList();
                if (invalid.length === 0) {
                    Toast.success(msg);
                } else {
                    Toast.error(msg);
                }
            } catch (e) {
                document.getElementById('moderationBlocklist_status').textContent = JSON.stringify(e.response || e, null, 2);
                Toast.error('Lint failed');
            }
        }

        function renderBlocklistInvalidList() {
            const container = document.getElementById('moderationBlocklist_invalidList');
            const onlyInvalid = !!document.getElementById('moderationBlocklist_onlyInvalid')?.checked;
            const actions = document.getElementById('moderationBlocklist_invalidActions');
            if (!onlyInvalid) { container.innerHTML = ''; if (actions) actions.style.display = 'none'; return; }
            const res = window._moderationBlocklistLastLint;
            if (!res || !Array.isArray(res.items)) { container.innerHTML = '<em>No lint results yet</em>'; return; }
            const invalid = (res.items || []).filter(it => it && it.ok === false);
            if (!invalid.length) { container.innerHTML = '<em>No invalid items</em>'; if (actions) actions.style.display = 'none'; return; }
            let html = '<table class="simple-table"><thead><tr><th>#</th><th>Type</th><th>Error</th><th>Line</th></tr></thead><tbody>';
            for (const it of invalid) {
                const idx = typeof it.index === 'number' ? it.index : '';
                const type = it.pattern_type || '';
                const err = it.error || 'invalid';
                const line = (it.line || '').slice(0, 120);
                html += `<tr>
                    <td>${idx}</td>
                    <td>${Utils.escapeHtml(String(type))}</td>
                    <td class="lint-invalid">${Utils.escapeHtml(String(err))}</td>
                    <td><code>${Utils.escapeHtml(String(line))}</code></td>
                </tr>`;
            }
            html += '</tbody></table>';
            container.innerHTML = html;
            if (actions) actions.style.display = 'block';
        }

        async function moderationCopyInvalidBlocklist() {
            try {
                const res = window._moderationBlocklistLastLint;
                if (!res || !Array.isArray(res.items)) { Toast.error('No lint results'); return; }
                const invalid = (res.items || []).filter(it => it && it.ok === false && typeof it.line === 'string');
                if (!invalid.length) { Toast.error('No invalid lines'); return; }
                const text = invalid.map(it => it.line).join('\n');
                if (navigator.clipboard && navigator.clipboard.writeText) {
                    await navigator.clipboard.writeText(text);
                    Toast.success('Copied invalid lines');
                } else {
                    // Fallback
                    const ta = document.createElement('textarea');
                    ta.value = text;
                    document.body.appendChild(ta);
                    ta.select();
                    const ok = document.execCommand('copy');
                    document.body.removeChild(ta);
                    if (ok) Toast.success('Copied invalid lines'); else Toast.error('Copy failed');
                }
            } catch (e) {
                Toast.error('Copy failed');
            }
        }
    </script>
    -->

    <div class="endpoint-section" id="moderationOverrides">
        <h2>
            <span class="endpoint-method get">GET/PUT/DELETE</span>
            <span class="endpoint-path">/api/v1/moderation/users/{user_id} - Per-user Overrides</span>
        </h2>
        <p>Configure user-specific moderation behavior. Empty fields leave global defaults in place.</p>

        <div class="columns">
            <div class="column">
                <div class="form-group">
                    <label for="modUserId">User ID:</label>
                    <input id="modUserId" type="text" placeholder="e.g., 1" />
                </div>
                <div class="form-group">
                    <label for="modEnabled">Enabled:</label>
                    <select id="modEnabled">
                        <option value="">(inherit)</option>
                        <option value="true">true</option>
                        <option value="false">false</option>
                    </select>
                </div>
                <div class="form-group">
                    <label for="modInputEnabled">Input Enabled:</label>
                    <select id="modInputEnabled">
                        <option value="">(inherit)</option>
                        <option value="true">true</option>
                        <option value="false">false</option>
                    </select>
                </div>
            </div>
            <div class="column">
                <div class="form-group">
                    <label for="modOutputEnabled">Output Enabled:</label>
                    <select id="modOutputEnabled">
                        <option value="">(inherit)</option>
                        <option value="true">true</option>
                        <option value="false">false</option>
                    </select>
                </div>
                <div class="form-group">
                    <label for="modInputAction">Input Action:</label>
                    <select id="modInputAction">
                        <option value="">(inherit)</option>
                        <option value="block">block</option>
                        <option value="redact">redact</option>
                        <option value="warn">warn</option>
                    </select>
                </div>
                <div class="form-group">
                    <label for="modOutputAction">Output Action:</label>
                    <select id="modOutputAction">
                        <option value="">(inherit)</option>
                        <option value="block">block</option>
                        <option value="redact">redact</option>
                        <option value="warn">warn</option>
                    </select>
                </div>
            </div>
        </div>
        <div class="form-group">
            <label for="modRedact">Redact Replacement:</label>
            <input id="modRedact" type="text" placeholder="[REDACTED]" />
        </div>

        <div class="btn-group">
            <button class="api-button" id="btnModOverrideLoad">Load</button>
            <button class="api-button" id="btnModOverrideSave">Save</button>
            <button class="api-button" id="btnModOverrideDelete">Delete</button>
        </div>

        <h3>Result:</h3>
        <pre id="moderationOverrides_result">---</pre>
    </div>

    <div class="endpoint-section" id="moderationOverridesList">
        <h2>
            <span class="endpoint-method get">GET</span>
            <span class="endpoint-path">/api/v1/moderation/users - List Overrides</span>
        </h2>
        <p>List all configured per-user overrides and load one into the editor above.</p>
        <div class="btn-group">
            <button class="api-button" id="btnModerationListOverrides">Load List</button>
        </div>
        <div id="moderationOverrides_table"></div>
    </div>

    <!-- inline script removed: now handled by js/admin-advanced.js -->
    <!--
    <script>
        async function moderationListOverrides() {
            try {
                const res = await apiClient.get('/api/v1/moderation/users');
                const overrides = (res && res.overrides) || {};
                const rows = Object.entries(overrides).map(([uid, o]) => ({ uid, ...o }));
                let html = '<table class="table"><thead><tr><th>User</th><th>enabled</th><th>input_enabled</th><th>output_enabled</th><th>input_action</th><th>output_action</th><th>redact_replacement</th><th>categories_enabled</th><th>Actions</th></tr></thead><tbody>';
                for (const r of rows) {
                    html += `<tr>
                        <td>${Utils.escapeHtml(String(r.uid))}</td>
                        <td>${String(r.enabled ?? '')}</td>
                        <td>${String(r.input_enabled ?? '')}</td>
                        <td>${String(r.output_enabled ?? '')}</td>
                        <td>${Utils.escapeHtml(String(r.input_action ?? ''))}</td>
                        <td>${Utils.escapeHtml(String(r.output_action ?? ''))}</td>
                        <td>${Utils.escapeHtml(String(r.redact_replacement ?? ''))}</td>
                        <td>${Utils.escapeHtml(String(r.categories_enabled ?? ''))}</td>
                        <td><button class="btn" onclick="moderationLoadIntoEditor('${r.uid}')">Load</button></td>
                    </tr>`;
                }
                html += '</tbody></table>';
                document.getElementById('moderationOverrides_table').innerHTML = html;
            } catch (e) {
                document.getElementById('moderationOverrides_table').innerHTML = `<pre>${Utils.escapeHtml(JSON.stringify(e.response || e, null, 2))}</pre>`;
            }
        }

        function moderationLoadIntoEditor(uid) {
            document.getElementById('modUserId').value = uid;
            loadUserOverride();
            Toast.success('Loaded override into editor');
        }
    </script>
    -->

    <div class="endpoint-section" id="moderationTester">
        <h2>
            <span class="endpoint-method post">POST</span>
            <span class="endpoint-path">/api/v1/moderation/test - Test Policy</span>
        </h2>
        <p>Test how the current policy behaves for a user and sample text.</p>
        <div class="columns">
            <div class="column">
                <div class="form-group">
                    <label for="modTest_user">User ID (optional):</label>
                    <input id="modTest_user" type="text" placeholder="e.g., 1" />
                </div>
            </div>
            <div class="column">
                <div class="form-group">
                    <label for="modTest_phase">Phase:</label>
                    <select id="modTest_phase">
                        <option value="input">input</option>
                        <option value="output">output</option>
                    </select>
                </div>
            </div>
        </div>
        <div class="form-group">
            <label for="modTest_text">Text:</label>
            <textarea id="modTest_text" rows="4" placeholder="Enter sample text to test..."></textarea>
        </div>
        <div class="btn-group">
            <button class="api-button" id="btnModerationRunTest">Test</button>
        </div>
        <h3>Result:</h3>
        <pre id="moderationTester_result">---</pre>
    </div>

    <!-- inline script removed: now handled by js/admin-advanced.js -->
    <!--
    <script>
        async function moderationRunTest() {
            try {
                const user_id = (document.getElementById('modTest_user').value || '').trim() || null;
                const phase = document.getElementById('modTest_phase').value;
                const text = document.getElementById('modTest_text').value || '';
                const res = await apiClient.post('/api/v1/moderation/test', { user_id, phase, text });
                document.getElementById('moderationTester_result').textContent = JSON.stringify(res, null, 2);
                Toast.success('Test completed');
            } catch (e) {
                document.getElementById('moderationTester_result').textContent = JSON.stringify(e.response || e, null, 2);
                Toast.error('Test failed');
            }
        }
    </script>
    -->

    <!-- inline script removed: now handled by js/admin-advanced.js -->
    <!--
    <script>
        function _buildOverridePayload() {
            const v = (id) => (document.getElementById(id)?.value ?? '').trim();
            const maybeBool = (x) => x === '' ? undefined : (x === 'true');
            const payload = {};
            const enabled = maybeBool(v('modEnabled'));
            const inp = maybeBool(v('modInputEnabled'));
            const outp = maybeBool(v('modOutputEnabled'));
            const ia = v('modInputAction');
            const oa = v('modOutputAction');
            const rr = v('modRedact');
            const cat = v('modUserCategories');
            if (enabled !== undefined) payload.enabled = enabled;
            if (inp !== undefined) payload.input_enabled = inp;
            if (outp !== undefined) payload.output_enabled = outp;
            if (ia) payload.input_action = ia;
            if (oa) payload.output_action = oa;
            if (rr) payload.redact_replacement = rr;
            if (cat) payload.categories_enabled = cat;
            return payload;
        }

        async function loadUserOverride() {
            try {
                const uid = (document.getElementById('modUserId').value || '').trim();
                if (!uid) { Toast.error('Enter a user ID'); return; }
                const res = await apiClient.get(`/api/v1/moderation/users/${uid}`);
                document.getElementById('moderationOverrides_result').textContent = JSON.stringify(res, null, 2);
                Toast.success('Loaded override');
            } catch (e) {
                document.getElementById('moderationOverrides_result').textContent = JSON.stringify(e.response || e, null, 2);
                Toast.error('Failed to load override');
            }
        }

        async function saveUserOverride() {
            try {
                const uid = (document.getElementById('modUserId').value || '').trim();
                if (!uid) { Toast.error('Enter a user ID'); return; }
                const payload = _buildOverridePayload();
                const res = await apiClient.put(`/api/v1/moderation/users/${uid}`, payload);
                document.getElementById('moderationOverrides_result').textContent = JSON.stringify(res, null, 2);
                Toast.success('Saved override');
            } catch (e) {
                document.getElementById('moderationOverrides_result').textContent = JSON.stringify(e.response || e, null, 2);
                Toast.error('Failed to save override');
            }
        }

        async function deleteUserOverride() {
            try {
                const uid = (document.getElementById('modUserId').value || '').trim();
                if (!uid) { Toast.error('Enter a user ID'); return; }
                if (!confirm('Delete override for user ' + uid + '?')) return;
                const res = await apiClient.delete(`/api/v1/moderation/users/${uid}`);
                document.getElementById('moderationOverrides_result').textContent = JSON.stringify(res, null, 2);
                Toast.success('Deleted override');
            } catch (e) {
                document.getElementById('moderationOverrides_result').textContent = JSON.stringify(e.response || e, null, 2);
                Toast.error('Failed to delete override');
            }
        }
    </script>
    -->
    </div>

    <div class="endpoint-section" id="adminUserGet">
        <h2>
            <span class="endpoint-method get">GET</span>
            <span class="endpoint-path">/api/v1/admin/users/{user_id} - Get User</span>
        </h2>
        <p>Get detailed information about a specific user.</p>

        <div class="form-group">
            <label for="adminUserGet_id">User ID <span class="required">*</span>:</label>
            <input type="text" id="adminUserGet_id" placeholder="user123">
        </div>

        <button class="api-button" id="btnAdminUserGet">
            Get User
        </button>

        <h3>cURL Command:</h3>
        <pre id="adminUserGet_curl">---</pre>

        <h3>Response:</h3>
        <pre id="adminUserGet_response">---</pre>
    </div>

    <div class="endpoint-section" id="adminUserApiKeys">
        <h2>
            <span class="endpoint-method get">GET/POST/DELETE</span>
            <span class="endpoint-path">/api/v1/admin/users/{user_id}/api-keys - Manage User API Keys</span>
        </h2>
        <p>List, create, rotate, or revoke API keys for a specific user.</p>

        <div class="columns">
            <div class="column">
                <div class="form-group">
                    <label for="adminApiKeys_userId">User ID <span class="required">*</span>:</label>
                    <input type="number" id="adminApiKeys_userId" placeholder="123" />
                </div>
                <div class="form-group">
                    <label for="adminApiKeys_includeRevoked">Include revoked/expired:</label>
                    <input type="checkbox" id="adminApiKeys_includeRevoked" />
                </div>
            </div>
            <div class="column">
                <div class="form-group">
                    <label for="adminApiKeys_name">Name:</label>
                    <input type="text" id="adminApiKeys_name" placeholder="Desktop Key" />
                </div>
                <div class="form-group">
                    <label for="adminApiKeys_scope">Scope:</label>
                    <select id="adminApiKeys_scope">
                        <option value="read">read</option>
                        <option value="write" selected>write</option>
                        <option value="admin">admin</option>
                    </select>
                </div>
                <div class="form-group">
                    <label for="adminApiKeys_expiry">Expires (days):</label>
                    <input type="number" id="adminApiKeys_expiry" value="365" min="1" />
                </div>
            </div>
        </div>

        <div class="btn-group">
            <button class="api-button" id="btnAdmUserKeysList">List Keys</button>
            <button class="api-button" id="btnAdmUserKeysCreate">Create Key</button>
        </div>

        <h3>Latest Result:</h3>
        <pre id="adminUserApiKeys_result">---</pre>

        <h3>Keys:</h3>
        <div id="adminUserApiKeys_list"></div>

        <h3>Update Limits</h3>
        <div class="columns">
            <div class="column">
                <div class="form-group">
                    <label for="adminApiKeys_keyId">Key ID:</label>
                    <input type="number" id="adminApiKeys_keyId" placeholder="456" />
                </div>
                <div class="form-group">
                    <label for="adminApiKeys_rate">Rate Limit (rpm):</label>
                    <input type="number" id="adminApiKeys_rate" placeholder="60" />
                </div>
            </div>
            <div class="column">
                <div class="form-group">
                    <label for="adminApiKeys_allowedIps">Allowed IPs (comma-separated):</label>
                    <input type="text" id="adminApiKeys_allowedIps" placeholder="127.0.0.1, 10.0.0.5" />
                </div>
            </div>
        </div>
        <div class="btn-group">
            <button class="api-button" id="btnAdmUserKeyUpdateLimits">Update Limits</button>
        </div>

        <h3>Audit Log</h3>
        <div class="btn-group">
            <button class="api-button" id="btnAdmUserKeyLoadAudit">Load Audit for Key</button>
        </div>
        <div id="adminUserApiKeys_audit"></div>
    </div>

    <!-- inline script removed: now handled by js/admin-advanced.js -->
    <!--
    <script>
        function _getAdminUserId() {
            const v = document.getElementById('adminApiKeys_userId').value;
            const id = parseInt(v, 10);
            if (!id) {
                Toast.error('Enter a valid user ID');
                throw new Error('Missing user id');
            }
            return id;
        }

        async function adminListUserApiKeys() {
            const userId = _getAdminUserId();
            const include = document.getElementById('adminApiKeys_includeRevoked').checked;
            try {
                const qs = include ? '?include_revoked=true' : '';
                const res = await apiClient.get(`/api/v1/admin/users/${userId}/api-keys${qs}`);
                renderAdminUserKeys(res || [], userId);
                document.getElementById('adminUserApiKeys_result').textContent = 'Loaded';
            } catch (e) {
                document.getElementById('adminUserApiKeys_result').textContent = JSON.stringify(e.response || e, null, 2);
                Toast.error('Failed to list keys');
            }
        }

        async function adminCreateUserApiKey() {
            const userId = _getAdminUserId();
            const payload = {
                name: document.getElementById('adminApiKeys_name').value || null,
                scope: document.getElementById('adminApiKeys_scope').value || 'write',
                expires_in_days: parseInt(document.getElementById('adminApiKeys_expiry').value || '365', 10)
            };
            try {
                const res = await apiClient.post(`/api/v1/admin/users/${userId}/api-keys`, payload);
                document.getElementById('adminUserApiKeys_result').textContent = JSON.stringify(res, null, 2);
                if (res && res.key) {
                    Toast.success('API key created. Copy it now; it is shown only once.');
                } else {
                    Toast.success('API key created.');
                }
                await adminListUserApiKeys();
            } catch (e) {
                document.getElementById('adminUserApiKeys_result').textContent = JSON.stringify(e.response || e, null, 2);
                Toast.error('Failed to create key');
            }
        }

        async function adminRotateUserApiKey(keyId, userId) {
            try {
                const res = await apiClient.post(`/api/v1/admin/users/${userId}/api-keys/${keyId}/rotate`, { expires_in_days: 365 });
                document.getElementById('adminUserApiKeys_result').textContent = JSON.stringify(res, null, 2);
                if (res && res.key) {
                    Toast.success('API key rotated. Copy the new key now.');
                } else {
                    Toast.success('API key rotated.');
                }
                await adminListUserApiKeys();
            } catch (e) {
                document.getElementById('adminUserApiKeys_result').textContent = JSON.stringify(e.response || e, null, 2);
                Toast.error('Failed to rotate key');
            }
        }

        async function adminRevokeUserApiKey(keyId, userId) {
            try {
                if (!confirm('Revoke this key?')) return;
                const res = await apiClient.delete(`/api/v1/admin/users/${userId}/api-keys/${keyId}`);
                document.getElementById('adminUserApiKeys_result').textContent = JSON.stringify(res, null, 2);
                Toast.success('API key revoked');
                await adminListUserApiKeys();
            } catch (e) {
                document.getElementById('adminUserApiKeys_result').textContent = JSON.stringify(e.response || e, null, 2);
                Toast.error('Failed to revoke key');
            }
        }

        function renderAdminUserKeys(items, userId) {
            const container = document.getElementById('adminUserApiKeys_list');
            if (!container) return;
            if (!items || !items.length) {
                container.innerHTML = '<p>No keys found.</p>';
                return;
            }
            let html = '<table class="simple-table"><thead><tr>' +
                '<th>ID</th><th>Name</th><th>Scope</th><th>Status</th><th>Created</th><th>Expires</th><th>Usage</th><th>Actions</th>' +
                '</tr></thead><tbody>';
            for (const k of items) {
                const id = Utils.escapeHtml(String(k.id));
                const nm = Utils.escapeHtml(String(k.name || ''));
                const sc = Utils.escapeHtml(String(k.scope || ''));
                const st = Utils.escapeHtml(String(k.status || ''));
                const ca = Utils.escapeHtml(String(k.created_at || ''));
                const ea = Utils.escapeHtml(String(k.expires_at || ''));
                const uc = Utils.escapeHtml(String(k.usage_count || 0));
                const uid = Utils.escapeHtml(String(userId));
                html += `
                <tr>
                    <td>${id}</td>
                    <td>${nm}</td>
                    <td>${sc}</td>
                    <td>${st}</td>
                    <td>${ca}</td>
                    <td>${ea}</td>
                    <td>${uc}</td>
                    <td>
                        <button class="btn adm-uk-rotate" data-uid="${uid}" data-kid="${id}">Rotate</button>
                        <button class="btn btn-danger adm-uk-revoke" data-uid="${uid}" data-kid="${id}">Revoke</button>
                    </td>
                </tr>`;
            }
            html += '</tbody></table>';
            container.innerHTML = html;
        }

        async function adminUpdateKeyLimits() {
            const userId = _getAdminUserId();
            const keyId = parseInt(document.getElementById('adminApiKeys_keyId').value || '0', 10);
            if (!keyId) { Toast.error('Enter a key id'); return; }
            const rate = document.getElementById('adminApiKeys_rate').value;
            const ipsRaw = document.getElementById('adminApiKeys_allowedIps').value;
            const allowed_ips = ipsRaw ? ipsRaw.split(',').map(s => s.trim()).filter(Boolean) : undefined;
            const payload = {};
            if (rate) payload.rate_limit = parseInt(rate, 10);
            if (allowed_ips !== undefined) payload.allowed_ips = allowed_ips;
            if (!Object.keys(payload).length) { Toast.error('Provide rate limit or allowed IPs'); return; }
            try {
                const res = await apiClient.patch(`/api/v1/admin/users/${userId}/api-keys/${keyId}`, payload);
                document.getElementById('adminUserApiKeys_result').textContent = JSON.stringify(res, null, 2);
                Toast.success('Key limits updated');
                await adminListUserApiKeys();
            } catch (e) {
                document.getElementById('adminUserApiKeys_result').textContent = JSON.stringify(e.response || e, null, 2);
                Toast.error('Failed to update limits');
            }
        }

        async function adminLoadKeyAudit() {
            const keyId = parseInt(document.getElementById('adminApiKeys_keyId').value || '0', 10);
            if (!keyId) { Toast.error('Enter a key id'); return; }
            try {
                const res = await apiClient.get(`/api/v1/admin/api-keys/${keyId}/audit-log`);
                const items = (res && res.items) ? res.items : [];
                const el = document.getElementById('adminUserApiKeys_audit');
                if (!items.length) { el.innerHTML = '<p>No audit events.</p>'; return; }
                let html = '<table class="simple-table"><thead><tr><th>Time</th><th>Action</th><th>User</th><th>IP</th><th>User Agent</th><th>Details</th></tr></thead><tbody>';
                for (const a of items) {
                    const ts = Utils.escapeHtml(String(a.created_at || ''));
                    const action = Utils.escapeHtml(String(a.action || ''));
                    const uid = Utils.escapeHtml(String(a.user_id ?? ''));
                    const ip = Utils.escapeHtml(String(a.ip_address || ''));
                    const ua = Utils.escapeHtml(String(a.user_agent || ''));
                    const det = Utils.escapeHtml(String(typeof a.details === 'object' ? JSON.stringify(a.details) : (a.details || '')));
                    html += `<tr>
                        <td>${ts}</td>
                        <td>${action}</td>
                        <td>${uid}</td>
                        <td>${ip}</td>
                        <td>${ua}</td>
                        <td><code>${det}</code></td>
                    </tr>`;
                }
                html += '</tbody></table>';
                el.innerHTML = html;
            } catch (e) {
                document.getElementById('adminUserApiKeys_result').textContent = JSON.stringify(e.response || e, null, 2);
                Toast.error('Failed to load audit log');
            }
        }
    </script>
    -->

    <div class="endpoint-section" id="adminUserUpdate">
        <h2>
            <span class="endpoint-method put">PUT</span>
            <span class="endpoint-path">/api/v1/admin/users/{user_id} - Update User</span>
        </h2>
        <p>Update user information and settings.</p>

        <div class="form-group">
            <label for="adminUserUpdate_id">User ID <span class="required">*</span>:</label>
            <input type="text" id="adminUserUpdate_id" placeholder="user123">
        </div>

        <div class="form-group">
            <label for="adminUserUpdate_payload">Update Payload (JSON):</label>
            <textarea id="adminUserUpdate_payload" class="code-input" rows="10">{
  "username": "updated_username",
  "email": "user@example.com",
  "role": "user",
  "status": "active",
  "settings": {
    "notifications": true,
    "theme": "dark"
  }
}</textarea>
        </div>

        <button class="api-button" id="btnAdminUserUpdate">
            Update User
        </button>

        <h3>cURL Command:</h3>
        <pre id="adminUserUpdate_curl">---</pre>

        <h3>Response:</h3>
        <pre id="adminUserUpdate_response">---</pre>
    </div>

    <div class="endpoint-section" id="adminUserDelete">
        <h2>
            <span class="endpoint-method delete">DELETE</span>
            <span class="endpoint-path">/api/v1/admin/users/{user_id} - Delete User</span>
        </h2>
        <p>Delete a user account.</p>

        <div class="form-group">
            <label for="adminUserDelete_id">User ID <span class="required">*</span>:</label>
            <input type="text" id="adminUserDelete_id" placeholder="user123">
        </div>

        <button class="api-button btn-danger" id="btnAdminUserDelete">
            Delete User
        </button>

        <h3>cURL Command:</h3>
        <pre id="adminUserDelete_curl">---</pre>

        <h3>Response:</h3>
        <pre id="adminUserDelete_response">---</pre>
    </div>
</div>

<!-- ============ Admin: Tool Catalogs (MCP) ============ -->
<div class="endpoint-section" id="adminToolCatalog">
    <h2>
        <span class="endpoint-method get">GET/POST/DELETE</span>
        <span class="endpoint-path">/api/v1/admin/mcp/tool_catalogs - MCP Tool Catalogs</span>
    </h2>
    <p>Create/list/delete tool catalogs and add/remove entries.</p>
    <div class="columns">
        <div class="column">
            <h3>Catalogs</h3>
            <div class="form-group"><label for="tc_name">Name:</label><input id="tc_name" type="text" placeholder="default"/></div>
            <div class="form-group"><label for="tc_desc">Description:</label><input id="tc_desc" type="text"/></div>
            <div class="form-group"><label for="tc_org">Org ID (optional):</label><input id="tc_org" type="number"/></div>
            <div class="form-group"><label for="tc_team">Team ID (optional):</label><input id="tc_team" type="number"/></div>
            <div class="btn-group">
                <button class="api-button" id="btnTCCreate">Create Catalog</button>
                <button class="api-button" id="btnTCList">List Catalogs</button>
            </div>
            <div class="form-group"><label for="tc_catalog_id">Catalog ID:</label><input id="tc_catalog_id" type="number"/></div>
            <div class="btn-group">
                <button class="api-button danger" id="btnTCDelete">Delete Catalog</button>
            </div>
            <div id="adminToolCatalog_list" style="margin-top:8px;"></div>
        </div>
        <div class="column">
            <h3>Entries</h3>
            <div class="form-group"><label for="tc_tool_name">Tool name:</label><input id="tc_tool_name" type="text" placeholder="tools.execute:media.index"/></div>
            <div class="form-group"><label for="tc_module_id">Module ID (optional):</label><input id="tc_module_id" type="text"/></div>
            <div class="btn-group">
                <button class="api-button" id="btnTCEntries">List Entries</button>
                <button class="api-button" id="btnTCAddEntry">Add Entry</button>
                <button class="api-button danger" id="btnTCDeleteEntry">Delete Entry</button>
            </div>
            <div id="adminToolCatalog_entries" style="margin-top:8px;"></div>
        </div>
    </div>
    <h3>Result:</h3>
    <pre id="adminToolCatalog_result">---</pre>
    <!-- bindings via js/admin-advanced.js -->
</div>

    <!-- External module bindings for Admin advanced panels -->
    <script type="module" src="js/admin-advanced.js"></script>
    <!-- External module: Admin RBAC core logic -->
    <script type="module" src="js/admin-rbac.js"></script>
    <!-- External module: Admin RBAC + Monitoring bindings -->
    <script type="module" src="js/admin-rbac-monitoring.js"></script>

<!-- System Status Tab -->
<div id="tabAdminSystem" class="tab-content">
    <div class="endpoint-section" id="adminSecurityAlerts">
        <h2>AuthNZ Security Alerts</h2>
        <p>Inspect delivery configuration, per-sink thresholds, and recent dispatch health.</p>
        <div class="btn-group">
            <button class="btn btn-primary" id="btnSecAlertRefresh">
                Refresh Status
            </button>
        </div>
        <div id="adminSecurityAlerts_health" style="margin-top: 10px; padding:6px 10px; border-radius:6px; display:inline-block; background:#d1d5db; color:#111;">--</div>
        <div class="table-responsive" style="margin-top: 15px;">
            <table class="table" id="adminSecurityAlerts_table">
                <thead>
                    <tr>
                        <th>Sink</th>
                        <th>Configured</th>
                        <th>Min Severity</th>
                        <th>Last Status</th>
                        <th>Last Error</th>
                        <th>Backoff Until</th>
                    </tr>
                </thead>
                <tbody>
                    <tr><td colspan="6" style="text-align:center;">No data</td></tr>
                </tbody>
            </table>
        </div>
        <h3>Raw Response:</h3>
        <pre id="adminSecurityAlerts_response">---</pre>

    </div>

    <div class="endpoint-section" id="adminSystemStatus">
        <h2>System Status and Health Checks</h2>
        <p>Monitor system health and status across all services.</p>

        <div class="btn-group">
            <button class="btn btn-primary" id="btnHealthMain">
                Main Health Check
            </button>
            <button class="btn btn-primary" id="btnHealthRAG">
                RAG Health
            </button>
            <button class="btn btn-primary" id="btnHealthEmbeddings">
                Embeddings Health
            </button>
            <button class="btn btn-primary" id="btnHealthWebScraping">
                Web Scraping Status
            </button>
        </div>

        <h3>Response:</h3>
        <pre id="adminSystemStatus_response">---</pre>
    </div>

    <div class="endpoint-section" id="adminCleanupSettings">
        <h2>Ephemeral Cleanup Settings</h2>
        <p>Configure the background worker that deletes expired ephemeral vector collections created during RAG pipeline evaluations.</p>
        <div class="columns">
            <div class="column">
                <div class="form-group">
                    <label for="adminCleanup_enabled">Enabled:</label>
                    <input type="checkbox" id="adminCleanup_enabled" />
                </div>
            </div>
            <div class="column">
                <div class="form-group">
                    <label for="adminCleanup_interval">Interval (seconds):</label>
                    <input type="number" id="adminCleanup_interval" min="60" max="604800" value="1800" />
                </div>
            </div>
        </div>
        <div class="btn-group">
            <button class="btn btn-primary" id="btnAdminCleanupLoad">Load Settings</button>
            <button class="btn btn-primary" id="btnAdminCleanupSave">Save Settings</button>
        </div>
        <h3>Result:</h3>
        <pre id="adminCleanupSettings_response">---</pre>
        <!-- inline script removed: now handled by js/admin-advanced.js -->
    </div>
</div>

    <!-- Usage Reporting Section -->
    <div class="endpoint-section" id="adminUsageReports">
        <h2>
            <span class="endpoint-method get">GET</span>
            <span class="endpoint-path">/api/v1/admin/usage/* - Usage Reports</span>
        </h2>
        <p>View aggregated usage (per-day) by user. Requires usage logging to be enabled.</p>

        <div class="columns">
            <div class="column">
                <div class="form-group">
                    <label for="usage_userId">User ID (optional):</label>
                    <input type="number" id="usage_userId" placeholder="e.g., 1" />
                </div>
                <div class="form-group">
                    <label for="usage_start">Start (YYYY-MM-DD):</label>
                    <input type="text" id="usage_start" placeholder="2025-01-01" />
                </div>
                <div class="form-group">
                    <label for="usage_end">End (YYYY-MM-DD):</label>
                    <input type="text" id="usage_end" placeholder="2025-01-31" />
                </div>
            </div>
            <div class="column">
                <div class="form-group">
                    <label for="usage_page">Page:</label>
                    <input type="number" id="usage_page" value="1" min="1" />
                </div>
                <div class="form-group">
                    <label for="usage_limit">Limit:</label>
                    <input type="number" id="usage_limit" value="50" min="1" max="500" />
                </div>
                <div class="form-group">
                    <label for="usage_top_limit">Top limit:</label>
                    <input type="number" id="usage_top_limit" value="10" min="1" max="100" />
                </div>
            </div>
        </div>

        <div class="btn-group">
            <button class="api-button" id="btnUsageLoadDaily">Load Daily</button>
            <button class="api-button" id="btnUsageDownloadDailyCSV">Download Daily CSV</button>
            <label style="margin-left:10px; display:inline-flex; align-items:center; gap:6px;">
                <input type="checkbox" id="usage_show_bytes_in" /> Show Bytes In
            </label>
            <span style="margin-left: 20px;"></span>
            <select id="usage_top_metric" title="Top metric">
                <option value="requests" selected>requests</option>
                <option value="bytes_total">bytes_total</option>
                <option value="bytes_in_total">bytes_in_total</option>
                <option value="errors">errors</option>
            </select>
            <button class="api-button" id="btnUsageTop">Top Users</button>
            <button class="api-button" id="btnUsageDownloadTopCSV">Download Top CSV</button>
        </div>

        <h3>Daily Usage</h3>
        <div id="adminUsageDaily_summary" class="muted"></div>
        <pre id="adminUsageDaily_raw" style="display:none">---</pre>
        <div id="adminUsageDaily_table"></div>

        <h3>Top Users</h3>
        <div id="adminUsageTop_summary" class="muted"></div>
        <pre id="adminUsageTop_raw" style="display:none">---</pre>
        <div id="adminUsageTop_table"></div>

        <hr />
        <h3>Manual Aggregate Day</h3>
        <div class="columns">
            <div class="column">
                <div class="form-group">
                    <label for="usage_agg_day">Day (YYYY-MM-DD):</label>
                    <input type="text" id="usage_agg_day" placeholder="2025-01-15" />
                </div>
            </div>
            <div class="column">
                <div class="btn-group" style="margin-top: 20px;">
                    <button class="api-button" id="btnUsageAggregate">Aggregate</button>
                </div>
            </div>
        </div>
        <pre id="adminUsageAgg_result">---</pre>

        <!-- inline script removed: now handled by js/admin-advanced.js -->
        <!--
        <script>
            function _usageQS() {
                const params = new URLSearchParams();
                const uid = parseInt(document.getElementById('usage_userId').value || '');
                const start = document.getElementById('usage_start').value.trim();
                const end = document.getElementById('usage_end').value.trim();
                const page = parseInt(document.getElementById('usage_page').value || '1', 10);
                const limit = parseInt(document.getElementById('usage_limit').value || '50', 10);
                if (!isNaN(uid)) params.set('user_id', String(uid));
                if (start) params.set('start', start);
                if (end) params.set('end', end);
                if (page) params.set('page', String(page));
                if (limit) params.set('limit', String(limit));
                return params.toString();
            }

            function _renderDailyTable(items) {
                if (!Array.isArray(items) || items.length === 0) {
                    return '<p>No data yet.</p>';
                }
                const showIn = !!(document.getElementById('usage_show_bytes_in') && document.getElementById('usage_show_bytes_in').checked);
                let html = '<table class="simple-table"><thead><tr>' +
                    '<th>User ID</th><th>Day</th><th>Requests</th><th>Errors</th><th>Bytes</th>' + (showIn ? '<th>Bytes In</th>' : '') + '<th>Avg Latency (ms)</th>' +
                    '</tr></thead><tbody>';
                for (const r of items) {
                    html += `<tr>
                        <td>${r.user_id}</td>
                        <td>${r.day}</td>
                        <td>${r.requests}</td>
                        <td>${r.errors}</td>
                        <td>${r.bytes_total}</td>
                        ${showIn ? `<td>${(r.bytes_in_total ?? '')}</td>` : ''}
                        <td>${r.latency_avg_ms ?? ''}</td>
                    </tr>`;
                }
                html += '</tbody></table>';
                return html;
            }

            function _renderTopTable(items) {
                if (!Array.isArray(items) || items.length === 0) {
                    return '<p>No data yet.</p>';
                }
                let html = '<table class="simple-table"><thead><tr>' +
                    '<th>User ID</th><th>Requests</th><th>Errors</th><th>Bytes</th><th>Avg Latency (ms)</th>' +
                    '</tr></thead><tbody>';
                for (const r of items) {
                    html += `<tr>
                        <td>${r.user_id}</td>
                        <td>${r.requests}</td>
                        <td>${r.errors}</td>
                        <td>${r.bytes_total}</td>
                        <td>${r.latency_avg_ms ?? ''}</td>
                    </tr>`;
                }
                html += '</tbody></table>';
                return html;
            }

            async function adminLoadUsageDaily() {
                try {
                    const qs = _usageQS();
                    const res = await apiClient.get(`/api/v1/admin/usage/daily?${qs}`);
                    document.getElementById('adminUsageDaily_raw').textContent = JSON.stringify(res, null, 2);
                    const items = Array.isArray(res.items) ? res.items : [];
                    document.getElementById('adminUsageDaily_table').innerHTML = _renderDailyTable(items);
                    // Update summary with selected range/page/limit
                    const start = (document.getElementById('usage_start').value || '').trim() || 'all';
                    const end = (document.getElementById('usage_end').value || '').trim() || 'all';
                    const page = (document.getElementById('usage_page').value || '1');
                    const limit = (document.getElementById('usage_limit').value || '50');
                    // Compute basic totals for current page
                    const totalRows = items.length;
                    const sumRequests = items.reduce((a, r) => a + (Number(r.requests) || 0), 0);
                    const sumErrors = items.reduce((a, r) => a + (Number(r.errors) || 0), 0);
                    const sumBytes = items.reduce((a, r) => a + (Number(r.bytes_total) || 0), 0);
                    const hasBytesIn = items.some(r => r.bytes_in_total !== undefined && r.bytes_in_total !== null);
                    const sumBytesIn = hasBytesIn ? items.reduce((a, r) => a + (Number(r.bytes_in_total) || 0), 0) : null;
                    const totalsPart = hasBytesIn
                        ? `| Rows: ${totalRows} | Requests: ${sumRequests} | Errors: ${sumErrors} | Bytes: ${sumBytes} | Bytes In: ${sumBytesIn}`
                        : `| Rows: ${totalRows} | Requests: ${sumRequests} | Errors: ${sumErrors} | Bytes: ${sumBytes}`;
                    const s = `Range: ${start} to ${end} | Page: ${page} | Limit: ${limit} ${totalsPart}`;
                    const el = document.getElementById('adminUsageDaily_summary');
                    if (el) el.textContent = s;
                    Toast.success('Loaded daily usage');
                } catch (e) {
                    document.getElementById('adminUsageDaily_table').innerHTML = '';
                    Toast.error('Failed to load daily usage');
                }
            }

            function adminDownloadUsageDailyCSV() {
                const params = new URLSearchParams(_usageQS());
                const start = (document.getElementById('usage_start').value || '').trim() || 'all';
                const end = (document.getElementById('usage_end').value || '').trim() || 'all';
                const showIn = !!(document.getElementById('usage_show_bytes_in') && document.getElementById('usage_show_bytes_in').checked);
                const suffix = showIn ? '_with-bytes-in' : '';
                const fname = `usage_daily_${start}_${end}${suffix}.csv`;
                params.set('filename', fname);
                const url = `/api/v1/admin/usage/daily/export.csv?${params.toString()}`;
                window.open(url, '_blank');
            }

            async function adminLoadUsageTop() {
                try {
                    const params = new URLSearchParams();
                    const start = document.getElementById('usage_start').value.trim();
                    const end = document.getElementById('usage_end').value.trim();
                    const topLimit = parseInt(document.getElementById('usage_top_limit').value || '10', 10);
                    const metric = (document.getElementById('usage_top_metric') || {}).value || 'requests';
                    if (start) params.set('start', start);
                    if (end) params.set('end', end);
                    if (topLimit) params.set('limit', String(topLimit));
                    if (metric) params.set('metric', metric);
                    const res = await apiClient.get(`/api/v1/admin/usage/top?${params.toString()}`);
                    document.getElementById('adminUsageTop_raw').textContent = JSON.stringify(res, null, 2);
                    document.getElementById('adminUsageTop_table').innerHTML = _renderTopTable(res.items || []);
                    // Update summary for top users
                    const s = `Top metric: ${metric} | Limit: ${topLimit}`;
                    const el = document.getElementById('adminUsageTop_summary');
                    if (el) el.textContent = s;
                    Toast.success('Loaded top users');
                } catch (e) {
                    document.getElementById('adminUsageTop_table').innerHTML = '';
                    Toast.error('Failed to load top users');
                }
            }

            function adminDownloadUsageTopCSV() {
                const params = new URLSearchParams();
                const start = (document.getElementById('usage_start').value || '').trim() || 'all';
                const end = (document.getElementById('usage_end').value || '').trim() || 'all';
                const topLimit = parseInt(document.getElementById('usage_top_limit').value || '10', 10);
                const metric = (document.getElementById('usage_top_metric') || {}).value || 'requests';
                if (start && start !== 'all') params.set('start', start);
                if (end && end !== 'all') params.set('end', end);
                if (topLimit) params.set('limit', String(topLimit));
                if (metric) params.set('metric', metric);
                const fname = `usage_top_${metric}_${start}_${end}.csv`;
                params.set('filename', fname);
                const url = `/api/v1/admin/usage/top/export.csv?${params.toString()}`;
                window.open(url, '_blank');
            }

            async function adminRunUsageAggregate() {
                try {
                    const day = document.getElementById('usage_agg_day').value.trim();
                    const qs = day ? `?day=${encodeURIComponent(day)}` : '';
                    const res = await apiClient.post(`/api/v1/admin/usage/aggregate${qs}`, {});
                    document.getElementById('adminUsageAgg_result').textContent = JSON.stringify(res, null, 2);
                    Toast.success('Aggregation requested');
                } catch (e) {
                    document.getElementById('adminUsageAgg_result').textContent = JSON.stringify(e.response || e, null, 2);
                    Toast.error('Failed to run aggregation');
                }
            }
        </script>
        -->
    </div>
</div>

<!-- Access Control (RBAC) Tab -->
<div id="tabAccessControl" class="tab-content">
    <div class="endpoint-section">
        <h2>Access Control</h2>
        <p>Manage roles, permissions, and per-user overrides.</p>
        <div class="columns">
            <div class="column">
                <h3>Roles</h3>
                <div class="form-group">
                    <label for="rbacRoleName">New Role Name:</label>
                    <input type="text" id="rbacRoleName" placeholder="analyst" />
                </div>
                <div class="form-group">
                    <label for="rbacRoleDesc">Description:</label>
                    <input type="text" id="rbacRoleDesc" placeholder="Role description" />
                </div>
                <div class="btn-group">
                    <button class="api-button" id="btnRbacCreateRole">Create Role</button>
                    <button class="api-button" id="btnRbacListRoles">List Roles</button>
                </div>
                <pre id="rbacRolesOut">---</pre>
            </div>
            <div class="column">
                <h3>Permissions</h3>
                <div class="form-group">
                    <label for="rbacPermName">New Permission (e.g., media.read):</label>
                    <input type="text" id="rbacPermName" placeholder="module.action" />
                </div>
                <div class="form-group">
                    <label for="rbacPermCat">Category:</label>
                    <input type="text" id="rbacPermCat" placeholder="media" />
                </div>
                <div class="btn-group">
                    <button class="api-button" id="btnRbacCreatePermission">Create Permission</button>
                    <button class="api-button" id="btnRbacListPermissions">List Permissions</button>
                </div>
                <pre id="rbacPermsOut">---</pre>
            </div>
        </div>

        <hr />

        <h3>User Assignments</h3>
        <div class="columns">
            <div class="column">
                <h4>Role Effective Permissions</h4>
                <div class="form-group">
                    <label for="rbacEffRoleId">Role ID:</label>
                    <input type="number" id="rbacEffRoleId" placeholder="e.g., 2" />
                </div>
                <div class="btn-group">
                    <button class="api-button" id="btnRbacGetRoleEffective">Get Role Effective Permissions</button>
                </div>
                <pre id="rbacRoleEffOut">---</pre>

                <div class="form-group">
                    <label for="rbacUserId">User ID:</label>
                    <input type="number" id="rbacUserId" placeholder="123" />
                </div>
                <div class="btn-group">
                    <button class="api-button" id="btnRbacGetUserRoles">Get Roles</button>
                    <button class="api-button" id="btnRbacGetEffectivePerms">Effective Permissions</button>
                </div>
                <pre id="rbacUserRolesOut">---</pre>
                <pre id="rbacUserEffPermsOut">---</pre>
            </div>
            <div class="column">
                <div class="form-group">
                    <label for="rbacAssignRoleId">Role ID to assign:</label>
                    <input type="number" id="rbacAssignRoleId" placeholder="5" />
                </div>
                <div class="btn-group">
                    <button class="api-button" id="btnRbacAssignRole">Assign Role</button>
                    <button class="api-button" id="btnRbacRemoveRole">Remove Role</button>
                </div>

                <h4>Overrides</h4>
                <div class="form-group">
                    <label for="rbacOverridePerm">Permission (name or id):</label>
                    <input type="text" id="rbacOverridePerm" placeholder="media.delete or 17" />
                </div>
                <div class="form-group">
                    <label for="rbacOverrideEffect">Effect:</label>
                    <select id="rbacOverrideEffect">
                        <option value="allow">allow</option>
                        <option value="deny">deny</option>
                    </select>
                </div>
                <div class="btn-group">
                    <button class="api-button" id="btnRbacUpsertOverride">Upsert Override</button>
                    <button class="api-button" id="btnRbacListOverrides">List Overrides</button>
                </div>
                <pre id="rbacOverridesOut">---</pre>
            </div>
        </div>
    </div>

    <!-- RBAC inline logic removed; now handled by js/admin-rbac.js -->
    <!--
    <script>
        async function rbacGetRoleEffective() {
            const roleIdRaw = document.getElementById('rbacEffRoleId').value.trim();
            if (!roleIdRaw) return Toast.error('Role ID is required');
            const roleId = parseInt(roleIdRaw, 10);
            if (isNaN(roleId) || roleId <= 0) return Toast.error('Enter a valid Role ID');
            try {
                const data = await apiClient.get(`/api/v1/admin/roles/${roleId}/permissions/effective`);
                document.getElementById('rbacRoleEffOut').textContent = JSON.stringify(data, null, 2);
                Toast.success('Loaded role effective permissions');
            } catch (e) {
                document.getElementById('rbacRoleEffOut').textContent = JSON.stringify(e.response || e, null, 2);
                Toast.error('Failed to load role effective permissions');
            }
        }
        function rbacViewEffectiveForRole(roleId) {
            try {
                const input = document.getElementById('rbacEffRoleId');
                if (input) input.value = String(roleId);
                rbacGetRoleEffective();
                const out = document.getElementById('rbacRoleEffOut');
                if (out && out.scrollIntoView) out.scrollIntoView({ behavior: 'smooth', block: 'start' });
            } catch (_) { /* no-op */ }
        }
        async function rbacListRoles() {
            const res = await apiClient.get('/api/v1/admin/roles');
            document.getElementById('rbacRolesOut').textContent = JSON.stringify(res, null, 2);
        }
        async function rbacCreateRole() {
            const name = document.getElementById('rbacRoleName').value.trim();
            const description = document.getElementById('rbacRoleDesc').value.trim() || null;
            if (!name) return Toast.error('Role name required');
            const res = await apiClient.post('/api/v1/admin/roles', { name, description });
            Toast.success('Role created');
            document.getElementById('rbacRolesOut').textContent = JSON.stringify(res, null, 2);
        }
        async function rbacListPermissions() {
            const res = await apiClient.get('/api/v1/admin/permissions');
            document.getElementById('rbacPermsOut').textContent = JSON.stringify(res, null, 2);
        }
        async function rbacCreatePermission() {
            const name = document.getElementById('rbacPermName').value.trim();
            const category = document.getElementById('rbacPermCat').value.trim() || null;
            if (!name) return Toast.error('Permission name required');
            const res = await apiClient.post('/api/v1/admin/permissions', { name, category });
            Toast.success('Permission created');
            document.getElementById('rbacPermsOut').textContent = JSON.stringify(res, null, 2);
        }
        function _rbacUserId() {
            const v = parseInt(document.getElementById('rbacUserId').value, 10);
            if (!v) throw new Error('User ID required');
            return v;
        }
        async function rbacGetUserRoles() {
            const uid = _rbacUserId();
            const res = await apiClient.get(`/api/v1/admin/users/${uid}/roles`);
            document.getElementById('rbacUserRolesOut').textContent = JSON.stringify(res, null, 2);
        }
        async function rbacAssignRole() {
            const uid = _rbacUserId();
            const rid = parseInt(document.getElementById('rbacAssignRoleId').value, 10);
            if (!rid) return Toast.error('Role ID required');
            const res = await apiClient.post(`/api/v1/admin/users/${uid}/roles/${rid}`, {});
            Toast.success('Role assigned');
            document.getElementById('rbacUserRolesOut').textContent = JSON.stringify(res, null, 2);
        }
        async function rbacRemoveRole() {
            const uid = _rbacUserId();
            const rid = parseInt(document.getElementById('rbacAssignRoleId').value, 10);
            if (!rid) return Toast.error('Role ID required');
            const res = await apiClient.delete(`/api/v1/admin/users/${uid}/roles/${rid}`);
            Toast.success('Role removed');
            document.getElementById('rbacUserRolesOut').textContent = JSON.stringify(res, null, 2);
        }
        async function rbacListOverrides() {
            const uid = _rbacUserId();
            const res = await apiClient.get(`/api/v1/admin/users/${uid}/overrides`);
            document.getElementById('rbacOverridesOut').textContent = JSON.stringify(res, null, 2);
        }
        async function rbacUpsertOverride() {
            const uid = _rbacUserId();
            const permField = document.getElementById('rbacOverridePerm').value.trim();
            const effect = document.getElementById('rbacOverrideEffect').value;
            if (!permField) return Toast.error('Permission required');
            let body = { effect };
            if (/^\d+$/.test(permField)) body.permission_id = parseInt(permField, 10);
            else body.permission_name = permField;
            const res = await apiClient.post(`/api/v1/admin/users/${uid}/overrides`, body);
            Toast.success('Override saved');
            document.getElementById('rbacOverridesOut').textContent = JSON.stringify(res, null, 2);
        }
        async function rbacGetEffectivePerms() {
            const uid = _rbacUserId();
            const res = await apiClient.get(`/api/v1/admin/users/${uid}/effective-permissions`);
            document.getElementById('rbacUserEffPermsOut').textContent = JSON.stringify(res, null, 2);
        }
    </script>
    -->

    <div class="endpoint-section" id="adminRbacMatrix">
        <h3>RBAC Matrix</h3>
        <p>Inspect role-to-permission assignments in a single view.</p>

        <div class="columns" style="margin-bottom:8px;">
            <div class="column">
                <label for="rbacMatrixCategory">Category:</label>
                <select id="rbacMatrixCategory">
                    <option value="">All</option>
                </select>
            </div>
            <div class="column">
                <label for="rbacVisibleCats">Visible Categories (client-side):</label>
                <select id="rbacVisibleCats" multiple size="4" style="min-width: 160px;"></select>
            </div>
            <div class="column">
                <label for="rbacMatrixSearch">Search:</label>
                <input type="text" id="rbacMatrixSearch" placeholder="filter permissions by name (substring)" />
            </div>
            <div class="column">
                <label for="rbacMatrixPinned">Pin columns (prefix list):</label>
                <input type="text" id="rbacMatrixPinned" placeholder="e.g. media., users.read" />
            </div>
            <div class="column">
                <label for="rbacRoleNames">Role names (exact):</label>
                <select id="rbacRoleNames" multiple size="4" style="min-width: 180px;"></select>
                <div class="btn-group" style="margin-top:6px;">
                    <button class="api-button" id="btnRbacSelectAllRoles">Select All</button>
                    <button class="api-button" id="btnRbacClearRoles">Clear</button>
                </div>
            </div>
            <div class="column">
                <label for="rbacRoleSearch">Role search:</label>
                <input type="text" id="rbacRoleSearch" placeholder="filter roles by name (substring)" />
            </div>
            <div class="column">
                <label for="rbacRolesLimit">Roles limit:</label>
                <input type="number" id="rbacRolesLimit" value="100" min="1" max="1000" />
            </div>
            <div class="column">
                <label for="rbacRolesOffset">Roles offset:</label>
                <input type="number" id="rbacRolesOffset" value="0" min="0" />
            </div>
        </div>

        <div class="btn-group" style="margin-bottom:8px;">
            <button class="api-button" id="btnRbacLoadList">Load Role→Permissions (list)</button>
            <button class="api-button" id="btnRbacLoadBoolean">Load RBAC Matrix (boolean grid)</button>
        </div>

        <div class="columns" style="align-items:center; margin: 4px 0 12px 0;">
            <div class="column">
                <span id="rbacRolesInfo">Roles: -</span>
                <span id="rbacRolesNote" style="opacity:0.75; margin-left:8px;"></span>
            </div>
            <div class="column">
                <div class="btn-group" style="justify-content:flex-end;">
                    <button class="api-button" id="rbacPrevBtn">Prev</button>
                    <button class="api-button" id="rbacNextBtn">Next</button>
                    <button class="api-button" id="btnRbacReload">Reload</button>
                    <button class="api-button" id="btnRbacExportMatrix">Export CSV</button>
                    <button class="api-button" id="btnRbacExportList">Export List CSV</button>
                    <button class="api-button" id="btnRbacCopySummary">Copy Summary</button>
                    <button class="api-button" id="btnRbacClearFilters">Clear Filters</button>
                </div>
            </div>
        </div>

        <div class="columns">
            <div class="column">
                <h4>Boolean Grid</h4>
                <div id="rbacMatrixBoolean"></div>
            </div>
            <div class="column">
                <h4>Per-Role Permissions</h4>
                <div id="rbacMatrixList"></div>
            </div>
        </div>
    </div>

    <!-- RBAC inline rendering removed; now handled by js/admin-rbac.js -->
    <!--
    <script>
        const RBAC_CACHE = { list: null, boolean: null, categoriesLoaded: false };
        const RBAC_META = { categories: [], permCategoryByName: {} };
        function _escapeHtml(text) {
            const map = { '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#039;' };
            return String(text).replace(/[&<>"']/g, m => map[m]);
        }

        async function loadRbacMatrixList() {
            try {
                await _ensurePermissionCategories();
                await _ensureRolesList();
                _setLoading('rbacMatrixList', true);
                const { category, search } = _filters();
                const qs = new URLSearchParams();
                if (category) qs.set('category', category);
                if (search) qs.set('search', search);
                const roleSearch = (document.getElementById('rbacRoleSearch').value || '').trim();
                const rolesLimit = parseInt(document.getElementById('rbacRolesLimit').value || '100', 10);
                const rolesOffset = parseInt(document.getElementById('rbacRolesOffset').value || '0', 10);
                if (roleSearch) qs.set('role_search', roleSearch);
                if (!isNaN(rolesLimit)) qs.set('roles_limit', String(rolesLimit));
                if (!isNaN(rolesOffset)) qs.set('roles_offset', String(rolesOffset));
                const roleNamesSel = document.getElementById('rbacRoleNames');
                const selectedNames = Array.from(roleNamesSel.selectedOptions || []).map(o => o.value);
                for (const n of selectedNames) qs.append('role_names', n);
                const path = qs.toString() ? `/api/v1/admin/roles/matrix?${qs.toString()}` : '/api/v1/admin/roles/matrix';
                const data = await apiClient.get(path);
                RBAC_CACHE.list = data;
                _rebuildPermCategoryByName();
                RBAC_CACHE.total_roles = typeof data.total_roles === 'number' ? data.total_roles : undefined;
                renderRbacMatrixList();
                _updateRbacRolesInfo(Array.isArray(data.roles) ? data.roles.length : 0);
                _saveRbacFilterState();
                Toast.success('Loaded RBAC role→permissions list');
            } catch (e) {
                document.getElementById('rbacMatrixList').innerHTML = `<pre>${_escapeHtml(JSON.stringify(e.response || e, null, 2))}</pre>`;
                Toast.error('Failed to load matrix');
            }
        }

        async function loadRbacMatrixBoolean() {
            try {
                await _ensurePermissionCategories();
                await _ensureRolesList();
                _setLoading('rbacMatrixBoolean', true);
                if (!RBAC_CACHE.list) {
                    // Build category mapping for client-side visibility
                    await loadRbacMatrixList();
                }
                const { category, search } = _filters();
                const qs = new URLSearchParams();
                if (category) qs.set('category', category);
                if (search) qs.set('search', search);
                const roleSearch = (document.getElementById('rbacRoleSearch').value || '').trim();
                const rolesLimit = parseInt(document.getElementById('rbacRolesLimit').value || '100', 10);
                const rolesOffset = parseInt(document.getElementById('rbacRolesOffset').value || '0', 10);
                if (roleSearch) qs.set('role_search', roleSearch);
                if (!isNaN(rolesLimit)) qs.set('roles_limit', String(rolesLimit));
                if (!isNaN(rolesOffset)) qs.set('roles_offset', String(rolesOffset));
                const roleNamesSel = document.getElementById('rbacRoleNames');
                const selectedNames = Array.from(roleNamesSel.selectedOptions || []).map(o => o.value);
                for (const n of selectedNames) qs.append('role_names', n);
                const path = qs.toString() ? `/api/v1/admin/roles/matrix-boolean?${qs.toString()}` : '/api/v1/admin/roles/matrix-boolean';
                const data = await apiClient.get(path);
                RBAC_CACHE.boolean = data;
                RBAC_CACHE.total_roles = typeof data.total_roles === 'number' ? data.total_roles : RBAC_CACHE.total_roles;
                renderRbacMatrixBoolean();
                _updateRbacRolesInfo(Array.isArray(data.roles) ? data.roles.length : 0);
                _saveRbacFilterState();
                Toast.success('Loaded RBAC boolean matrix');
            } catch (e) {
                document.getElementById('rbacMatrixBoolean').innerHTML = `<pre>${_escapeHtml(JSON.stringify(e.response || e, null, 2))}</pre>`;
                Toast.error('Failed to load boolean matrix');
            }
        }

        function reloadRbacMatrices() {
            // Reload both views with current filter + pagination
            loadRbacMatrixList();
            loadRbacMatrixBoolean();
        }

        function _rebuildPermCategoryByName() {
            try {
                const list = RBAC_CACHE.list;
                const perms = (list && Array.isArray(list.permissions)) ? list.permissions : [];
                const map = {};
                for (const p of perms) {
                    if (!p || !p.name) continue;
                    map[p.name] = p.category || '';
                }
                RBAC_META.permCategoryByName = map;
            } catch (_) { /* ignore */ }
        }

        function rbacSelectAllRoleNames() {
            try {
                const sel = document.getElementById('rbacRoleNames');
                for (const opt of Array.from(sel.options || [])) opt.selected = true;
                _saveRbacFilterState();
                reloadRbacMatrices();
            } catch (_) { /* ignore */ }
        }

        function rbacClearRoleNames() {
            try {
                const sel = document.getElementById('rbacRoleNames');
                for (const opt of Array.from(sel.options || [])) opt.selected = false;
                _saveRbacFilterState();
                reloadRbacMatrices();
            } catch (_) { /* ignore */ }
        }

        function _getRolesLimitOffset() {
            const limit = parseInt(document.getElementById('rbacRolesLimit').value || '100', 10);
            const offset = parseInt(document.getElementById('rbacRolesOffset').value || '0', 10);
            return { limit: isNaN(limit) ? 100 : limit, offset: isNaN(offset) ? 0 : offset };
        }

        function _setRolesOffset(newOffset) {
            if (newOffset < 0) newOffset = 0;
            document.getElementById('rbacRolesOffset').value = String(newOffset);
        }

        function _updateRbacRolesInfo(rolesReturned) {
            const info = document.getElementById('rbacRolesInfo');
            const note = document.getElementById('rbacRolesNote');
            const total = RBAC_CACHE.total_roles || 0;
            const { limit, offset } = _getRolesLimitOffset();
            const start = total ? Math.min(total, offset + 1) : (offset + 1);
            const end = total ? Math.min(total, offset + rolesReturned) : (offset + rolesReturned);
            const text = total ? `Roles: ${start}-${end} of ${total}` : `Roles: ${start}-${end}`;
            info.textContent = text;
            // Update pager disabled states
            const prevBtn = document.getElementById('rbacPrevBtn');
            const nextBtn = document.getElementById('rbacNextBtn');
            if (prevBtn) prevBtn.disabled = offset <= 0;
            if (nextBtn) nextBtn.disabled = (total && offset + rolesReturned >= total) || rolesReturned === 0;
            if (note) {
                if (total && end < total) {
                    if (offset === 0) note.textContent = `Showing first ${end} of ${total}. Use Next to see more.`;
                    else note.textContent = `More roles available. Use pager to navigate.`;
                } else {
                    note.textContent = '';
                }
            }
        }

        function rbacPrevPage() {
            const { limit, offset } = _getRolesLimitOffset();
            const newOffset = Math.max(0, offset - limit);
            _setRolesOffset(newOffset);
            reloadRbacMatrices();
        }

        function rbacNextPage() {
            const { limit, offset } = _getRolesLimitOffset();
            const total = RBAC_CACHE.total_roles || 0;
            const newOffset = (total && offset + limit >= total) ? offset : offset + limit;
            _setRolesOffset(newOffset);
            reloadRbacMatrices();
        }

        function clearRbacFilters() {
            try {
                document.getElementById('rbacMatrixCategory').value = '';
                document.getElementById('rbacMatrixSearch').value = '';
                document.getElementById('rbacMatrixPinned').value = '';
                document.getElementById('rbacRoleSearch').value = '';
                document.getElementById('rbacRolesLimit').value = '100';
                document.getElementById('rbacRolesOffset').value = '0';
                const sel = document.getElementById('rbacRoleNames');
                for (const opt of Array.from(sel.options || [])) opt.selected = false;
                _saveRbacFilterState();
                reloadRbacMatrices();
            } catch (_) { /* ignore */ }
        }

        async function exportRbacMatrixCsv() {
            try {
                // Ensure boolean matrix is loaded for export
                if (!RBAC_CACHE.boolean) {
                    await loadRbacMatrixBoolean();
                    if (!RBAC_CACHE.boolean) return;
                }
                const data = RBAC_CACHE.boolean;
                const roles = Array.isArray(data.roles) ? data.roles : [];
                if (!roles.length) {
                    Toast.error('No roles to export');
                    return;
                }
                // Apply same filtered/reordered permission names as current view
                let permNames = Array.isArray(data.permission_names) ? data.permission_names.slice() : [];
                const { category, search, pinned } = _filters();
                if (category && window.RBAC_META && RBAC_META.categories) {
                    // We don't have category-by-name map now; rely on server-side filtering having removed unmatching perms
                }
                if (search) {
                    const s = search.toLowerCase();
                    permNames = permNames.filter(n => n.toLowerCase().includes(s));
                }
                if (pinned && pinned.length) {
                    const isPinned = (name) => pinned.some(p => name.startsWith(p));
                    const left = permNames.filter(isPinned);
                    const right = permNames.filter(n => !isPinned(n));
                    permNames = [...left, ...right];
                }
                const matrix = Array.isArray(data.matrix) ? data.matrix : [];
                const originalNames = Array.isArray(data.permission_names) ? data.permission_names : [];
                const nameToIndex = new Map();
                for (let i = 0; i < originalNames.length; i++) nameToIndex.set(originalNames[i], i);

                const csvEscape = (v) => '"' + String(v).replace(/"/g, '""') + '"';
                let csv = '';
                // Header
                csv += [csvEscape('Role \\ Permission'), ...permNames.map(csvEscape)].join(',') + '\n';
                // Rows
                for (let r = 0; r < roles.length; r++) {
                    const role = roles[r];
                    const fullRow = matrix[r] || [];
                    const cells = [csvEscape(role.name || '')];
                    for (const pn of permNames) {
                        const idx = nameToIndex.get(pn);
                        const v = idx != null ? !!fullRow[idx] : false;
                        cells.push(v ? '1' : '0');
                    }
                    csv += cells.join(',') + '\n';
                }
                const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = 'rbac_matrix.csv';
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                URL.revokeObjectURL(url);
                Toast.success('Matrix CSV downloaded');
            } catch (e) {
                console.error(e);
                Toast.error('Failed to export CSV');
            }
        }

        async function exportRbacListCsv() {
            try {
                // Ensure list is loaded
                if (!RBAC_CACHE.list) {
                    await loadRbacMatrixList();
                    if (!RBAC_CACHE.list) return;
                }
                const data = RBAC_CACHE.list;
                const roles = Array.isArray(data.roles) ? data.roles : [];
                const perms = Array.isArray(data.permissions) ? data.permissions : [];
                const grants = new Set((data.grants || []).map(g => `${g.role_id}:${g.permission_id}`));
                if (!roles.length) {
                    Toast.error('No roles to export');
                    return;
                }
                // Build aggregated list
                const permIdToName = {};
                for (const p of perms) permIdToName[p.id] = p.name;
                const csvEscape = (v) => '"' + String(v).replace(/"/g, '""') + '"';
                let csv = '';
                csv += [csvEscape('Role'), csvEscape('Permissions')].join(',') + '\n';
                for (const role of roles) {
                    const names = [];
                    for (const p of perms) {
                        if (grants.has(`${role.id}:${p.id}`)) names.push(permIdToName[p.id] || p.name);
                    }
                    csv += [csvEscape(role.name || ''), csvEscape(names.join('; '))].join(',') + '\n';
                }
                const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = 'rbac_list.csv';
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                URL.revokeObjectURL(url);
                Toast.success('List CSV downloaded');
            } catch (e) {
                console.error(e);
                Toast.error('Failed to export list CSV');
            }
        }

        async function copyRbacSummary() {
            try {
                // Prefer list view for readable summary
                if (!RBAC_CACHE.list) {
                    await loadRbacMatrixList();
                    if (!RBAC_CACHE.list) return;
                }
                const data = RBAC_CACHE.list;
                const roles = Array.isArray(data.roles) ? data.roles : [];
                const perms = Array.isArray(data.permissions) ? data.permissions : [];
                const grants = new Set((data.grants || []).map(g => `${g.role_id}:${g.permission_id}`));
                const permIdToName = {};
                for (const p of perms) permIdToName[p.id] = p.name;
                let text = '';
                for (const role of roles) {
                    const names = [];
                    for (const p of perms) {
                        if (grants.has(`${role.id}:${p.id}`)) names.push(permIdToName[p.id] || p.name);
                    }
                    text += `${role.name}: ${names.join(', ')}` + '\n';
                }
                await navigator.clipboard.writeText(text);
                Toast.success('Summary copied to clipboard');
            } catch (e) {
                console.error(e);
                Toast.error('Failed to copy summary');
            }
        }

        function _setLoading(targetId, isLoading) {
            try {
                const target = document.getElementById(targetId);
                if (!target) return;
                if (isLoading) {
                    target.innerHTML = '<div class="spinner" aria-label="Loading"></div>';
                }
            } catch (_) { /* ignore */ }
        }

        function rbacShowOnlyCategory() {
            try {
                const cat = (document.getElementById('rbacPinCategory').value || '').trim();
                document.getElementById('rbacMatrixCategory').value = cat;
                _saveRbacFilterState();
                reloadRbacMatrices();
            } catch (_) { /* ignore */ }
        }

        function rbacPinCategory() {
            try {
                const cat = (document.getElementById('rbacPinCategory').value || '').trim();
                if (!cat) return;
                const field = document.getElementById('rbacMatrixPinned');
                const prefix = cat + '.';
                const parts = (field.value || '').split(',').map(s => s.trim()).filter(Boolean);
                if (!parts.includes(prefix)) parts.unshift(prefix);
                field.value = parts.join(',');
                _saveRbacFilterState();
                if (RBAC_CACHE.boolean) renderRbacMatrixBoolean();
            } catch (_) { /* ignore */ }
        }

        function rbacUnpinCategory() {
            try {
                const cat = (document.getElementById('rbacPinCategory').value || '').trim();
                if (!cat) return;
                const field = document.getElementById('rbacMatrixPinned');
                const prefix = cat + '.';
                const parts = (field.value || '').split(',').map(s => s.trim()).filter(Boolean);
                const filtered = parts.filter(p => p !== prefix);
                field.value = filtered.join(',');
                _saveRbacFilterState();
                if (RBAC_CACHE.boolean) renderRbacMatrixBoolean();
            } catch (_) { /* ignore */ }
        }

        async function exportRbacGrantsJson() {
            try {
                await _ensurePermissionCategories();
                const { category, search } = _filters();
                const qs = new URLSearchParams();
                if (category) qs.set('category', category);
                if (search) qs.set('search', search);
                const roleSearch = (document.getElementById('rbacRoleSearch').value || '').trim();
                const rolesLimit = parseInt(document.getElementById('rbacRolesLimit').value || '100', 10);
                const rolesOffset = parseInt(document.getElementById('rbacRolesOffset').value || '0', 10);
                if (roleSearch) qs.set('role_search', roleSearch);
                if (!isNaN(rolesLimit)) qs.set('roles_limit', String(rolesLimit));
                if (!isNaN(rolesOffset)) qs.set('roles_offset', String(rolesOffset));
                const roleNamesSel = document.getElementById('rbacRoleNames');
                const selectedNames = Array.from(roleNamesSel.selectedOptions || []).map(o => o.value);
                for (const n of selectedNames) qs.append('role_names', n);
                const path = qs.toString() ? `/api/v1/admin/roles/matrix?${qs.toString()}` : '/api/v1/admin/roles/matrix';
                const payload = await apiClient.get(path);
                const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json;charset=utf-8;' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = 'rbac_grants.json';
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                URL.revokeObjectURL(url);
                Toast.success('Grants JSON downloaded');
            } catch (e) {
                console.error(e);
                Toast.error('Failed to export grants JSON');
            }
        }

        async function _ensureRolesList() {
            try {
                if (RBAC_CACHE.rolesLoaded) return;
                const res = await apiClient.get('/api/v1/admin/roles');
                const sel = document.getElementById('rbacRoleNames');
                const current = new Set(Array.from(sel.selectedOptions || []).map(o => o.value));
                let html = '';
                for (const r of (res || [])) {
                    const name = r.name || '';
                    const selected = current.has(name) ? ' selected' : '';
                    html += `<option value="${_escapeHtml(name)}"${selected}>${_escapeHtml(name)}</option>`;
                }
                sel.innerHTML = html;
                RBAC_CACHE.rolesLoaded = true;
                _applySavedRoleNamesSelection();
            } catch (_) { /* ignore */ }
        }

        function _saveRbacFilterState() {
            try {
                const data = {
                    category: document.getElementById('rbacMatrixCategory').value || '',
                    search: document.getElementById('rbacMatrixSearch').value || '',
                    pinned: document.getElementById('rbacMatrixPinned').value || '',
                    roleSearch: document.getElementById('rbacRoleSearch').value || '',
                    rolesLimit: document.getElementById('rbacRolesLimit').value || '100',
                    rolesOffset: document.getElementById('rbacRolesOffset').value || '0',
                    roleNames: Array.from((document.getElementById('rbacRoleNames').selectedOptions || [])).map(o => o.value),
                    visibleCats: Array.from((document.getElementById('rbacVisibleCats').selectedOptions || [])).map(o => o.value),
                };
                localStorage.setItem('rbac_matrix_filters', JSON.stringify(data));
            } catch (_) { /* ignore */ }
        }

        function _loadRbacFilterState() {
            try {
                const raw = localStorage.getItem('rbac_matrix_filters');
                if (!raw) return null;
                const data = JSON.parse(raw);
                if (!data || typeof data !== 'object') return null;
                document.getElementById('rbacMatrixCategory').value = data.category || '';
                document.getElementById('rbacMatrixSearch').value = data.search || '';
                document.getElementById('rbacMatrixPinned').value = data.pinned || '';
                document.getElementById('rbacRoleSearch').value = data.roleSearch || '';
                document.getElementById('rbacRolesLimit').value = data.rolesLimit || '100';
                document.getElementById('rbacRolesOffset').value = data.rolesOffset || '0';
                RBAC_CACHE.savedRoleNames = Array.isArray(data.roleNames) ? data.roleNames : [];
                RBAC_CACHE.savedVisibleCats = Array.isArray(data.visibleCats) ? data.visibleCats : [];
                return data;
            } catch (_) { return null; }
        }

        function _applySavedRoleNamesSelection() {
            try {
                const saved = RBAC_CACHE.savedRoleNames || [];
                if (!saved.length) return;
                const sel = document.getElementById('rbacRoleNames');
                const options = Array.from(sel.options || []);
                for (const opt of options) opt.selected = saved.includes(opt.value);
            } catch (_) { /* ignore */ }
        }

        function _applySavedVisibleCatsSelection() {
            try {
                const saved = RBAC_CACHE.savedVisibleCats || [];
                if (!saved.length) return;
                const sel = document.getElementById('rbacVisibleCats');
                const options = Array.from(sel.options || []);
                for (const opt of options) opt.selected = saved.includes(opt.value);
            } catch (_) { /* ignore */ }
        }

        (function initRbacFilters() {
            _loadRbacFilterState();
            _ensurePermissionCategories();
            _ensureRolesList();
            try {
                const ids = ['rbacMatrixCategory','rbacMatrixSearch','rbacMatrixPinned','rbacRoleSearch','rbacRolesLimit','rbacRolesOffset','rbacRoleNames'];
                for (const id of ids) {
                    const el = document.getElementById(id);
                    if (!el) continue;
                    const ev = (id === 'rbacRoleNames' || id === 'rbacMatrixCategory') ? 'change' : 'input';
                    el.addEventListener(ev, () => _saveRbacFilterState());
                }
                const vis = document.getElementById('rbacVisibleCats');
                if (vis) vis.addEventListener('change', () => _saveRbacFilterState());
            } catch (_) { /* ignore */ }
        })();

        function _filters() {
            const category = document.getElementById('rbacMatrixCategory').value || '';
            const search = (document.getElementById('rbacMatrixSearch').value || '').trim();
            const pinned = (document.getElementById('rbacMatrixPinned').value || '')
                .split(',').map(s => s.trim()).filter(Boolean);
            return { category, search, pinned };
        }

async function _ensurePermissionCategories() {
            if (RBAC_CACHE.categoriesLoaded) return;
            try {
                const cats = await apiClient.get('/api/v1/admin/permissions/categories');
                RBAC_META.categories = Array.isArray(cats) ? cats : [];
            } catch (_) {
                RBAC_META.categories = [];
            }
            const sel = document.getElementById('rbacMatrixCategory');
            const current = sel.value;
            let html = '<option value="">All</option>';
            for (const c of RBAC_META.categories) html += `<option value="${_escapeHtml(c)}">${_escapeHtml(c)}</option>`;
            sel.innerHTML = html;
    if (RBAC_META.categories.includes(current)) sel.value = current;
    RBAC_CACHE.categoriesLoaded = true;
    // Also populate quick pin/unpin category select if present
    try {
        const pinSel = document.getElementById('rbacPinCategory');
        if (pinSel) {
            let ph = '';
            for (const c of RBAC_META.categories) ph += `<option value="${_escapeHtml(c)}">${_escapeHtml(c)}</option>`;
            pinSel.innerHTML = ph;
        }
    } catch (_) { /* ignore */ }
}

        function renderRbacMatrixBoolean() {
            const data = RBAC_CACHE.boolean;
            const container = document.getElementById('rbacMatrixBoolean');
            const roles = Array.isArray(data.roles) ? data.roles : [];
            let permNames = Array.isArray(data.permission_names) ? data.permission_names.slice() : [];
            const matrix = Array.isArray(data.matrix) ? data.matrix : [];
            if (!roles.length || !permNames.length) {
                container.innerHTML = '<p>No roles or permissions.</p>';
                return;
            }
            const { category, search, pinned } = _filters();
            // Apply client-side visible categories filter (independent of server filter)
            try {
                const visSel = document.getElementById('rbacVisibleCats');
                const vis = visSel ? Array.from(visSel.selectedOptions || []).map(o => o.value) : [];
                if (vis && vis.length && RBAC_META.permCategoryByName) {
                    permNames = permNames.filter(n => vis.includes(RBAC_META.permCategoryByName[n] || ''));
                }
            } catch (_) { /* ignore */ }
            // Keep server category filter behavior if set and mapping present
            if (category && RBAC_META.permCategoryByName) permNames = permNames.filter(n => (RBAC_META.permCategoryByName[n] || '') === category);
            if (search) {
                const s = search.toLowerCase();
                permNames = permNames.filter(n => n.toLowerCase().includes(s));
            }
            if (pinned.length) {
                const isPinned = (name) => pinned.some(p => name.startsWith(p));
                const left = permNames.filter(isPinned);
                const right = permNames.filter(n => !isPinned(n));
                permNames = [...left, ...right];
            }
            // Build table
            let html = '<div class="scroll-x"><table class="simple-table small-table">';
            html += '<thead><tr><th class="rbac-sticky-left header">Role \\ Permission</th>' + permNames.map(n => `<th>${_escapeHtml(n)}</th>`).join('') + '</tr></thead>';
            html += '<tbody>';
            for (let r = 0; r < roles.length; r++) {
                const role = roles[r];
                const fullRow = matrix[r] || [];
                const nameToIndex = new Map();
                const originalNames = RBAC_CACHE.boolean.permission_names;
                for (let i = 0; i < originalNames.length; i++) nameToIndex.set(originalNames[i], i);
                html += `<tr><td class="rbac-sticky-left"><strong>${_escapeHtml(role.name)}</strong> <button class="btn btn-secondary btn-compact" title="View effective permissions" aria-label="View effective permissions for role ${_escapeHtml(role.name)}" onclick="rbacViewEffectiveForRole(${role.id})">Eff</button></td>`;
                for (let c = 0; c < permNames.length; c++) {
                    const idx = nameToIndex.get(permNames[c]);
                    const v = idx != null ? !!fullRow[idx] : false;
                    html += `<td style="text-align:center;">${v ? '✓' : ''}</td>`;
                }
                html += '</tr>';
            }
            html += '</tbody></table></div>';
            container.innerHTML = html;
        }

        function renderRbacMatrixList() {
            const data = RBAC_CACHE.list;
            const container = document.getElementById('rbacMatrixList');
            const roles = Array.isArray(data.roles) ? data.roles : [];
            let perms = Array.isArray(data.permissions) ? data.permissions.slice() : [];
            const grants = new Set((data.grants || []).map(g => `${g.role_id}:${g.permission_id}`));
            if (!roles.length || !perms.length) {
                container.innerHTML = '<p>No roles or permissions.</p>';
                return;
            }
            const { category, search, pinned } = _filters();
            if (category) perms = perms.filter(p => (p.category || '') === category);
            if (search) {
                const s = search.toLowerCase();
                perms = perms.filter(p => p.name.toLowerCase().includes(s));
            }
            if (pinned.length) {
                const isPinned = (name) => pinned.some(p => name.startsWith(p));
                const left = perms.filter(p => isPinned(p.name));
                const right = perms.filter(p => !isPinned(p.name));
                perms = [...left, ...right];
            }
            const permIdToName = {};
            for (const p of perms) permIdToName[p.id] = p.name;
            let html = '<div class="scroll-y" style="max-height:360px">';
            for (const role of roles) {
                const names = [];
                for (const p of perms) {
                    if (grants.has(`${role.id}:${p.id}`)) names.push(p.name);
                }
                html += `<div class="card"><div class="card-header"><strong>${_escapeHtml(role.name)}</strong> <button class="btn btn-secondary btn-compact" title="View effective permissions" aria-label="View effective permissions for role ${_escapeHtml(role.name)}" onclick="rbacViewEffectiveForRole(${role.id})">Eff</button></div>`;
                html += `<div class="card-body"><div>${names.length ? names.map(_escapeHtml).join(', ') : '<em>No permissions</em>'}</div></div></div>`;
            }
            html += '</div>';
            container.innerHTML = html;
        }
    </script>
    -->
</div>

<!-- User Permissions Editor (Admin) -->
<div id="tabAdminUserPermissions" class="tab-content">
    <div class="endpoint-section" id="adminUserPerms">
        <h2>User Permissions Editor</h2>
        <p>Search for a user, manage their roles, and set per-permission overrides. Admin access required.</p>

        <div class="columns" style="margin-bottom: 8px;">
            <div class="column">
                <label for="userPermSearch">Find User (name/email):</label>
                <input type="text" id="userPermSearch" placeholder="e.g., alice or alice@example.com" />
                <div class="btn-group" style="margin-top:6px;">
                    <button class="api-button" id="btnUserPermSearch">Search</button>
                    <button class="api-button" id="btnUserPermReload" title="Reload current user">Reload</button>
                </div>
            </div>
            <div class="column">
                <div id="userPermSelected" style="margin-top: 26px; opacity: .85;">
                    No user selected
                </div>
            </div>
        </div>

        <div id="userPermSearchResults" class="card" style="display:none; padding: 8px; margin-bottom: 10px;"></div>

        <div class="columns">
            <div class="column">
                <h3>Roles</h3>
                <div id="userPermRolesList" class="scroll-y" style="max-height: 260px;"></div>
            </div>
            <div class="column">
                <h3>Effective Permissions</h3>
                <div class="btn-group" style="margin-bottom: 6px;">
                    <button class="api-button" id="btnUserPermRefreshEffective">Refresh</button>
                </div>
                <pre id="userPermEffectiveOut" class="small-pre">---</pre>
            </div>
        </div>

        <h3 style="margin-top: 12px;">Overrides</h3>
        <div class="columns" style="margin-bottom: 6px;">
            <div class="column">
                <label for="permFilterSearch">Filter:</label>
                <input type="text" id="permFilterSearch" placeholder="search permissions (substring)" />
            </div>
            <div class="column">
                <label for="permFilterCategory">Category:</label>
                <select id="permFilterCategory"><option value="">All</option></select>
            </div>
        </div>

        <div class="scroll-y" style="max-height: 420px;">
            <h4>Tool Permissions</h4>
            <div class="btn-group" style="margin-bottom:6px;">
                <button class="api-button" id="btnPermBulkAllowTools" title="Allow all filtered tool permissions">Allow All (Tools)</button>
                <button class="api-button" id="btnPermBulkDenyTools" title="Deny all filtered tool permissions">Deny All (Tools)</button>
                <button class="api-button" id="btnPermBulkInheritTools" title="Inherit all filtered tool permissions">Inherit All (Tools)</button>
            </div>
            <table class="simple-table small-table">
                <thead>
                    <tr>
                        <th style="min-width: 280px;">Permission</th>
                        <th>Category</th>
                        <th>Effective</th>
                        <th style="min-width: 240px;">Override</th>
                    </tr>
                </thead>
                <tbody id="userPermOverridesTableTools"></tbody>
            </table>

            <h4 style="margin-top: 14px;">Standard Permissions</h4>
            <div class="btn-group" style="margin-bottom:6px;">
                <button class="api-button" id="btnPermBulkAllowStd" title="Allow all filtered standard permissions">Allow All (Standard)</button>
                <button class="api-button" id="btnPermBulkDenyStd" title="Deny all filtered standard permissions">Deny All (Standard)</button>
                <button class="api-button" id="btnPermBulkInheritStd" title="Inherit all filtered standard permissions">Inherit All (Standard)</button>
            </div>
            <table class="simple-table small-table">
                <thead>
                    <tr>
                        <th style="min-width: 280px;">Permission</th>
                        <th>Category</th>
                        <th>Effective</th>
                        <th style="min-width: 240px;">Override</th>
                    </tr>
                </thead>
                <tbody id="userPermOverridesTableStd"></tbody>
            </table>
        </div>

        <div id="userPermToastArea"></div>
    </div>

    <script type="module" src="js/admin-user-permissions.js"></script>
</div>

<!-- ============ Admin: Virtual Keys (per user) ============ -->
<div class="endpoint-section" id="adminVirtualKeys">
    <h2>
        <span class="endpoint-method get">GET/POST/DELETE</span>
        <span class="endpoint-path">/api/v1/admin/users/{user_id}/virtual-keys - Manage User Virtual Keys</span>
    </h2>
    <p>List and create scoped virtual keys (JWT-like) for a user. Revoke using the standard API key revoke endpoint.</p>
    <div class="columns">
        <div class="column">
            <div class="form-group">
                <label for="admVK_userId">User ID <span class="required">*</span>:</label>
                <input id="admVK_userId" type="number" placeholder="123" />
            </div>
            <div class="form-group">
                <label for="admVK_name">Name:</label>
                <input id="admVK_name" type="text" placeholder="CI Job Token" />
            </div>
            <div class="form-group">
                <label for="admVK_desc">Description:</label>
                <input id="admVK_desc" type="text" placeholder="automation token" />
            </div>
        </div>
        <div class="column">
            <div class="form-group">
                <label for="admVK_expires">Expires (days):</label>
                <input id="admVK_expires" type="number" value="30" min="1" />
            </div>
            <div class="form-group">
                <label for="admVK_endpoints">Allowed Endpoints (CSV):</label>
                <input id="admVK_endpoints" type="text" placeholder="chat.completions,embeddings" />
            </div>
            <div class="form-group">
                <label for="admVK_methods">Allowed Methods (CSV):</label>
                <input id="admVK_methods" type="text" placeholder="GET,POST" />
            </div>
        </div>
        <div class="column">
            <div class="form-group">
                <label for="admVK_paths">Allowed Paths (CSV):</label>
                <input id="admVK_paths" type="text" placeholder="/api/v1/chat/completions" />
            </div>
            <div class="form-group">
                <label for="admVK_calls">Max Calls:</label>
                <input id="admVK_calls" type="number" placeholder="e.g., 100" />
            </div>
            <div class="form-group">
                <label for="admVK_runs">Max Runs:</label>
                <input id="admVK_runs" type="number" placeholder="e.g., 1" />
            </div>
        </div>
    </div>
    <div class="btn-group">
        <button class="api-button" id="btnAdmVKList">List Virtual Keys</button>
        <button class="api-button" id="btnAdmVKCreate">Create Virtual Key</button>
    </div>
    <h3>Result:</h3>
    <pre id="adminVirtualKeys_result">---</pre>
    <div id="adminVirtualKeys_list"></div>
    <!-- bindings via js/admin-advanced.js -->
</div>

<!-- ============ Admin: Organizations & Teams ============ -->
<div class="endpoint-section" id="adminOrgsTeams">
    <h2>
        <span class="endpoint-method get">GET/POST</span>
        <span class="endpoint-path">/api/v1/admin/orgs & /teams - Organizations and Teams</span>
    </h2>
    <p>Create and list organizations, teams, and manage memberships. Also update org watchlist settings.</p>
    <div class="columns">
        <div class="column">
            <h3>Organizations</h3>
            <div class="form-group"><label for="org_name">Name:</label><input id="org_name" type="text" placeholder="Acme"/></div>
            <div class="form-group"><label for="org_slug">Slug:</label><input id="org_slug" type="text" placeholder="acme"/></div>
            <div class="form-group"><label for="org_owner">Owner User ID (optional):</label><input id="org_owner" type="number"/></div>
            <div class="btn-group">
                <button class="api-button" id="btnAdmCreateOrg">Create Org</button>
                <button class="api-button" id="btnAdmListOrgs">List Orgs</button>
            </div>
            <div id="adminOrgs_list" style="margin-top:8px;"></div>
        </div>
        <div class="column">
            <h3>Teams</h3>
            <div class="form-group"><label for="team_org">Org ID:</label><input id="team_org" type="number" placeholder="1"/></div>
            <div class="form-group"><label for="team_name">Team Name:</label><input id="team_name" type="text" placeholder="research"/></div>
            <div class="form-group"><label for="team_slug">Slug:</label><input id="team_slug" type="text" placeholder="research"/></div>
            <div class="btn-group">
                <button class="api-button" id="btnAdmCreateTeam">Create Team</button>
                <button class="api-button" id="btnAdmListTeams">List Teams</button>
            </div>
            <div id="adminTeams_list" style="margin-top:8px;"></div>
        </div>
        <div class="column">
            <h3>Memberships</h3>
            <div class="form-group"><label for="m_org">Org ID:</label><input id="m_org" type="number"/></div>
            <div class="form-group"><label for="m_team">Team ID:</label><input id="m_team" type="number"/></div>
            <div class="form-group"><label for="m_user">User ID:</label><input id="m_user" type="number"/></div>
            <div class="form-group"><label for="m_role">Role:</label><input id="m_role" type="text" placeholder="member"/></div>
            <div class="btn-group">
                <button class="api-button" id="btnAdmAddTeamMember">Add Team Member</button>
                <button class="api-button" id="btnAdmListTeamMembers">List Team Members</button>
                <button class="api-button danger" id="btnAdmRemoveTeamMember">Remove Team Member</button>
            </div>
            <div class="btn-group">
                <button class="api-button" id="btnAdmAddOrgMember">Add Org Member</button>
                <button class="api-button" id="btnAdmListOrgMembers">List Org Members</button>
                <button class="api-button" id="btnAdmUpdateOrgMemberRole">Update Org Member Role</button>
                <button class="api-button danger" id="btnAdmRemoveOrgMember">Remove Org Member</button>
            </div>
            <div class="form-group"><label for="org_wl_require">Org Watchlists require include default:</label>
                <select id="org_wl_require"><option value="">(no change)</option><option value="true">true</option><option value="false">false</option></select>
            </div>
            <div class="btn-group">
                <button class="api-button" id="btnAdmGetOrgWatchCfg">Get Org Watchlists Settings</button>
                <button class="api-button" id="btnAdmSetOrgWatchCfg">Update Org Watchlists Settings</button>
            </div>
        </div>
    </div>
    <h3>Result:</h3>
    <pre id="adminOrgsTeams_result">---</pre>
    <!-- bindings via js/admin-advanced.js -->
</div>

<!-- ============ Admin: Tool Permissions ============ -->
<div class="endpoint-section" id="adminToolPermissions">
    <h2>
        <span class="endpoint-method get">GET/POST/DELETE</span>
        <span class="endpoint-path">/api/v1/admin/permissions/tools - Tool Execute Permissions</span>
    </h2>
    <p>Manage tool execution permissions and grants to roles.</p>
    <div class="columns">
        <div class="column">
            <div class="form-group"><label for="tp_name">Tool Name:</label><input id="tp_name" type="text" placeholder="tools.execute:media.index"/></div>
            <div class="form-group"><label for="tp_desc">Description:</label><input id="tp_desc" type="text"/></div>
            <div class="btn-group">
                <button class="api-button" id="btnTPCreate">Create Permission</button>
                <button class="api-button" id="btnTPList">List Permissions</button>
                <button class="api-button danger" id="btnTPDelete">Delete Permission</button>
            </div>
        </div>
        <div class="column">
            <div class="form-group"><label for="tp_role">Role ID:</label><input id="tp_role" type="number"/></div>
            <div class="form-group"><label for="tp_tool">Tool (or *):</label><input id="tp_tool" type="text" placeholder="media.index or *"/></div>
            <div class="form-group"><label for="tp_prefix">Prefix:</label><input id="tp_prefix" type="text" placeholder="tools.execute:media."/></div>
            <div class="btn-group">
                <button class="api-button" id="btnTPGrant">Grant to Role</button>
                <button class="api-button danger" id="btnTPRevoke">Revoke from Role</button>
                <button class="api-button" id="btnTPPrefixGrant">Grant by Prefix</button>
                <button class="api-button danger" id="btnTPPrefixRevoke">Revoke by Prefix</button>
                <button class="api-button" id="btnTPListRole">List Role Tool Perms</button>
            </div>
        </div>
    </div>
    <h3>Result:</h3>
    <pre id="adminToolPermissions_result">---</pre>
    <div id="adminToolPermissions_list" style="margin-top:8px;"></div>
    <!-- bindings via js/admin-advanced.js -->
</div>

<!-- ============ Admin: Rate Limits ============ -->
<div class="endpoint-section" id="adminRateLimits">
    <h2>
        <span class="endpoint-method post">POST</span>
        <span class="endpoint-path">/api/v1/admin/rate-limits/* - Rate Limit Admin</span>
    </h2>
    <p>Upsert role/user rate limits and reset counters.</p>
    <div class="columns">
        <div class="column">
            <h3>Role Rate Limit</h3>
            <div class="form-group"><label for="rl_role">Role ID:</label><input id="rl_role" type="number"/></div>
            <div class="form-group"><label for="rl_resource">Resource:</label><input id="rl_resource" type="text" placeholder="chat.completions"/></div>
            <div class="form-group"><label for="rl_limit">Limit/min:</label><input id="rl_limit" type="number"/></div>
            <div class="form-group"><label for="rl_burst">Burst:</label><input id="rl_burst" type="number"/></div>
            <div class="btn-group"><button class="api-button" id="btnRLUpsertRole">Upsert Role Limit</button></div>
        </div>
        <div class="column">
            <h3>User Rate Limit</h3>
            <div class="form-group"><label for="rl_user">User ID:</label><input id="rl_user" type="number"/></div>
            <div class="form-group"><label for="rl_u_resource">Resource:</label><input id="rl_u_resource" type="text" placeholder="chat.completions"/></div>
            <div class="form-group"><label for="rl_u_limit">Limit/min:</label><input id="rl_u_limit" type="number"/></div>
            <div class="form-group"><label for="rl_u_burst">Burst:</label><input id="rl_u_burst" type="number"/></div>
            <div class="btn-group"><button class="api-button" id="btnRLUpsertUser">Upsert User Limit</button></div>
        </div>
        <div class="column">
            <h3>Reset Counters</h3>
            <div class="form-group"><label for="rl_kind">Kind:</label><select id="rl_kind"><option value="ip">ip</option><option value="user">user</option><option value="api">api</option><option value="raw">raw</option></select></div>
            <div class="form-group"><label for="rl_identifier">Identifier/IP/hash:</label><input id="rl_identifier" type="text" placeholder="ip: 203.0.113.7"/></div>
            <div class="form-group"><label for="rl_endpoint">Endpoint (optional):</label><input id="rl_endpoint" type="text" placeholder="/api/v1/media/process"/></div>
            <div class="form-group"><label for="rl_dry">Dry run:</label><select id="rl_dry"><option value="false">false</option><option value="true">true</option></select></div>
            <div class="btn-group"><button class="api-button" id="btnRLReset">Reset</button></div>
        </div>
    </div>
    <h3>Result:</h3>
    <pre id="adminRateLimits_result">---</pre>
    <!-- bindings via js/admin-advanced.js -->
</div>
